Browse Source

Add slug name for organisations.

tags/20180716
Sam Black 2 years ago
parent
commit
1171f43715
8 changed files with 184 additions and 48 deletions
  1. +40
    -0
      migrations/versions/056e315c9f5e_.py
  2. +9
    -0
      vowel/forms/organisation.py
  3. +1
    -0
      vowel/models/organisations.py
  4. +1
    -0
      vowel/templates/landing/signup.html
  5. +7
    -3
      vowel/templates/organisation/edit.html
  6. +28
    -0
      vowel/utils/organisation.py
  7. +86
    -36
      vowel/views/organisation.py
  8. +12
    -9
      vowel/web.py

+ 40
- 0
migrations/versions/056e315c9f5e_.py View File

@@ -0,0 +1,40 @@
"""
Add organisation slug data to construct URLs and subdomains.

Revision ID: 056e315c9f5e
Revises: c5dd2bd49172
Create Date: 2018-02-12 16:52:13.353669

"""

# revision identifiers, used by Alembic.
revision = '056e315c9f5e'
down_revision = 'c5dd2bd49172'

from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker

from vowel.models.organisations import Organisation


def upgrade():
bind = op.get_bind()
session = sessionmaker()(bind=bind)

# Slug is not nullable, but we make it that way after populating some data
op.add_column('organisation', sa.Column('slug', sa.String(length=255)))
op.create_unique_constraint(None, 'organisation', ['slug'])

# Populate some default data in Organisations.slug
# by taking the organisation name and just removing the spaces
for org in session.query(Organisation):
org.slug = org.name.lower().replace(" ", "")
session.commit()

op.alter_column("organisation", "slug", nullable=False)


def downgrade():
op.drop_constraint(None, 'organisation', type_='unique')
op.drop_column('organisation', 'slug')

+ 9
- 0
vowel/forms/organisation.py View File

@@ -28,6 +28,7 @@ from wtforms import StringField
from wtforms import TextAreaField
from wtforms.validators import DataRequired
from wtforms.validators import InputRequired
from wtforms.validators import Length

from vowel.forms.utils import HTMLInputField
from vowel.models.organisations import contact_choices
@@ -53,6 +54,10 @@ class OrganisationForm(FlaskForm):
contact_information = FieldList(FormField(OrganisationContactForm))

name = StringField(validators=[DataRequired(), InputRequired()])
slug = StringField(
"Short name",
validators=[DataRequired(), InputRequired(), Length(min=5)],
description="Shortened company name to use for your site address")
description = HTMLInputField()
address = TextAreaField()

@@ -66,6 +71,10 @@ class OrganisationForm(FlaskForm):
super().__init__(*args, **kwargs)
self.company_number.flags.placeholder = True

self.slug.flags.placeholder = True
self.slug.flags.add_hidden = True
self.slug.flags.add_hidden = True


class NewOrganisationForm(OrganisationForm):
"""


+ 1
- 0
vowel/models/organisations.py View File

@@ -67,6 +67,7 @@ class Organisation(db.Model):
server_default=sql_text("false"), nullable=False)

name = db.Column(db.String(255), unique=True, nullable=False)
slug = db.Column(db.String(255), unique=True, nullable=False)
description = db.Column(db.Text())

address = db.Column(db.Text())


+ 1
- 0
vowel/templates/landing/signup.html View File

@@ -29,6 +29,7 @@

<h4>Your company's information</h4>
{{ render_field(form.name) }}
{{ render_field(form.slug, extra_id="organisationSlugName") }}
{{ render_field(form.address, rows=6) }}

{{ render_field(form.submit, False, btn_class="primary") }}


+ 7
- 3
vowel/templates/organisation/edit.html View File

@@ -54,14 +54,18 @@
</div>
{% endif %}

<form name="organisation_edit"
action="{{ post_url }}" method="post">
<form name="organisation_edit" action="{{ post_url }}" method="post">
<div data-uk-grid>
<div class="uk-width-4-5@l">
{{ form.hidden_tag() }}
{{ render_field(form.name) }}
{% if current_user.has_role("Admin") %}
{{ render_field(form.name) }}
{{ render_field(form.slug, extra_id="organisationSlugName") }}
{{ render_field(form.enabled) }}
{% else %}
{# This isn't editable by the organisation owner #}
{{ render_field(form.name, static=True) }}
{{ render_field(form.slug, extra_id="organisationSlugName", static=True) }}
{% endif %}
{{ render_field(form.description, extra_id="description") }}
{{ render_field(form.address, rows=6) }}


+ 28
- 0
vowel/utils/organisation.py View File

@@ -37,6 +37,10 @@ from vowel.utils.queue import rq_manager
ORGANISATION_TEMPLATES = "organisation/email"


class OrganisationNotFound(Exception):
pass


@rq_manager.job()
def _mail_organisation(email, organisation_id):
"""
@@ -137,3 +141,27 @@ def organisation_from_request(view_args):
return assessment.module.organisation_id

return None


def organisation_from_id_or_slug(org_id=None, org_slug=None):
"""
Return the organisation from the provided ID or slug.

:param org_id: organisation ID
:type org_id: int
:param org_slug: organisation slug name
:type org_slug: str
:return: the corresponding organisation
:rtype: vowel.models.organisation.Organisation
"""
if org_id:
org = Organisation.query.get(org_id)
elif org_slug:
org = Organisation.query.filter_by(slug=org_slug).first()
else:
raise OrganisationNotFound("No organisation provided")

if org:
return org
else:
raise OrganisationNotFound("No organisation found")

+ 86
- 36
vowel/views/organisation.py View File

@@ -19,6 +19,7 @@
import arrow

from flask import Blueprint
from flask import abort
from flask import flash
from flask import redirect
from flask import render_template
@@ -44,6 +45,8 @@ from vowel.models.tokens import TokenPurchase
import vowel.utils.branding
from vowel.utils import cache
from vowel.utils.auth import is_staff_member
from vowel.utils.organisation import OrganisationNotFound
from vowel.utils.organisation import organisation_from_id_or_slug
from vowel.utils.tokens import update_token_types


@@ -51,17 +54,23 @@ organisation = Blueprint("organisations", __name__)


@organisation.route("/<int:organisation_id>/")
@organisation.route("/<organisation_slug>/")
@login_required
def view(organisation_id):
def view(organisation_id=None, organisation_slug=None):
"""
View lesson presentation using reveal.js
View organisation information

:param organisation_id: organisation to view
:param organisation_id: organisation to view, selected by ID
:type organisation_id: int
:param organisation_slug: organisation to view, selected by slug name
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
org = Organisation.query.filter_by(id=organisation_id).first_or_404()
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)

return render_template("organisation/view.html", organisation=org)

@@ -152,44 +161,60 @@ def _edit_organisation(organisation_id=None):


@organisation.route("/<int:organisation_id>/edit/", methods=["GET", "POST"])
@organisation.route("/<organisation_slug>/edit/", methods=["GET", "POST"])
@login_required
@is_staff_member()
def edit(organisation_id):
def edit(organisation_id=None, organisation_slug=None):
"""
Edit organisation.

:param organisation_id: organisation to view
:param organisation_id: organisation to edit, by ID
:type organisation_id: int
:param organisation_slug: organisation to edit, by slug name
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
return _edit_organisation(organisation_id)
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)

return _edit_organisation(org.id)


@organisation.route("/<int:organisation_id>/edit/branding/",
methods=["GET", "POST"])
@organisation.route("/<organisation_slug>/edit/branding/",
methods=["GET", "POST"])
@login_required
@is_staff_member()
def branding(organisation_id):
def branding(organisation_id=None, organisation_slug=None):
"""
Edit organisation branding.

:param organisation_id: organisation branding to edit
:param organisation_id: organisation branding to edit, by ID
:type organisation_id: int
:param organisation_slug: organisation branding to edit, by slug name
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
organisation = Organisation.query.get_or_404(organisation_id)
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)

org_branding = OrganisationBranding.query.filter_by(
organisation_id=organisation_id).first()
organisation_id=org.id).first()
if not org_branding:
org_branding = OrganisationBranding(organisation_id=organisation_id)
org_branding = OrganisationBranding(organisation_id=org.id)

form = OrganisationBrandingForm(obj=org_branding)

if org_branding.logo_id:
logo_url = url_for("resources.file_view",
organisation_id=organisation_id,
organisation_id=org.id,
purpose=org_branding.logo.purpose,
document_id=org_branding.logo_id,
_external=True)
@@ -203,7 +228,7 @@ def branding(organisation_id):
except ValueError:
flash("Logo could not be found", "danger")
return redirect(url_for("organisations.branding",
organisation_id=organisation_id))
organisation_id=org.id))

resource = Resource.query.get(logo_res_id)
if resource:
@@ -211,7 +236,7 @@ def branding(organisation_id):
else:
flash("Logo could not be found", "danger")
return redirect(url_for("organisations.branding",
organisation_id=organisation_id))
organisation_id=org.id))
elif form.logo_delete.data:
org_branding.logo_id = None

@@ -221,15 +246,14 @@ def branding(organisation_id):
db.session.commit()

# Drop the cached branding info as it has changed
cache.delete_memoized(vowel.utils.branding._branding, organisation_id)
cache.delete_memoized(vowel.utils.branding._branding, org.id)

flash("Organisation branding saved", "success")
return redirect(url_for("organisations.branding",
organisation_id=organisation_id))
organisation_id=org.id))

return render_template("organisation/branding.html",
organisation=organisation, form=form,
logo_url=logo_url)
organisation=org, form=form, logo_url=logo_url)


@organisation.route("/new/", methods=["GET", "POST"])
@@ -246,21 +270,28 @@ def new():


@organisation.route("/<int:organisation_id>/tokens/")
@organisation.route("/<organisation_slug>/tokens/")
@login_required
@is_staff_member()
def tokens(organisation_id):
def tokens(organisation_id=None, organisation_slug=None):
"""
Tokens for this organisation.

:param organisation_id: organisation identification
:type organisation_id: int
:param organisation_slug: organisation identification by slug
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
org = Organisation.query.get_or_404(organisation_id)
update_token_types(organisation_id)
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)

update_token_types(org.id)

org_tokens = Token.query.filter_by(organisation_id=organisation_id).all()
org_tokens = Token.query.filter_by(organisation_id=org.id).all()

return render_template("organisation/tokens.html", organisation=org,
tokens=org_tokens)
@@ -268,20 +299,27 @@ def tokens(organisation_id):

@organisation.route("/<int:organisation_id>/tokens/purchase/<int:token_id>/",
methods=["GET", "POST"])
@organisation.route("/<organisation_slug>/tokens/purchase/<int:token_id>/",
methods=["GET", "POST"])
@login_required
@is_staff_member()
def token_purchase(organisation_id, token_id):
def token_purchase(token_id, organisation_id=None, organisation_slug=None):
"""
Purchase tokens for this organisation.

:param organisation_id: organisation identification
:type organisation_id: int
:param token_id: token identification
:type token_id: int
:param organisation_id: organisation identification, by ID
:type organisation_id: int
:param organisation_slug: organisation identification, by slug
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
org = Organisation.query.get_or_404(organisation_id)
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)
token = Token.query.get_or_404(token_id)

token_form = TokenPurchaseForm()
@@ -315,7 +353,7 @@ def token_purchase(organisation_id, token_id):

flash("Tokens purchased", "success")
return redirect(url_for("organisations.tokens",
organisation_id=organisation_id))
organisation_id=org.id))
else:
token_form.currency.data = "GBP"

@@ -324,35 +362,47 @@ def token_purchase(organisation_id, token_id):


@organisation.route("/<int:organisation_id>/tokens/history/")
@organisation.route("/<organisation_slug>/tokens/history/")
@login_required
@is_staff_member()
def token_history(organisation_id):
def token_history(organisation_id=None, organisation_slug=None):
"""
Token purchase history

:param organisation_id: organisation
:param organisation_id: organisation by ID
:type organisation_id: int
:param organisation_slug: organisation by slug
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
org = Organisation.query.get_or_404(organisation_id)
update_token_types(organisation_id)
org_tokens = Token.query.filter_by(organisation_id=organisation_id).all()
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)
update_token_types(org.id)
org_tokens = Token.query.filter_by(organisation_id=org.id).all()

return render_template("organisation/tokens_list.html", organisation=org,
tokens=org_tokens)


@organisation.route("/<int:organisation_id>/tokens/payment_info/")
@organisation.route("/<organisation_slug>/tokens/payment_info/")
@login_required
@is_staff_member()
def token_payment_info(organisation_id):
def token_payment_info(organisation_id=None, organisation_slug=None):
"""
Token payment methods

:param organisation_id: organisation
:param organisation_id: organisation by ID
:type organisation_id: int
:param organisation_slug: organisation by slug
:type organisation_slug: str
:return: HTML output
:rtype: jinja2.Template
"""
pass
try:
org = organisation_from_id_or_slug(organisation_id, organisation_slug)
except OrganisationNotFound:
raise abort(404)

+ 12
- 9
vowel/web.py View File

@@ -273,17 +273,20 @@ def register_blueprints(app):
:param app: Flask application
:type app: flask.Flask
"""
# These apps register their url_prefix
app.register_blueprint(users)
app.register_blueprint(organisation)
app.register_blueprint(dashboard)
app.register_blueprint(courses)
app.register_blueprint(modules)
app.register_blueprint(lessons)
app.register_blueprint(assessment)
app.register_blueprint(analytics)
app.register_blueprint(resources)
app.register_blueprint(presentation)
app.register_blueprint(annotations)
# TODO: Register site admin app

app.register_blueprint(organisation, url_prefix="/organisation")
app.register_blueprint(courses, url_prefix="/course")
#app.register_blueprint(modules)
#app.register_blueprint(lessons)
#app.register_blueprint(assessment)
app.register_blueprint(analytics, url_prefix="/analytics")
#app.register_blueprint(resources)
#app.register_blueprint(presentation)
#app.register_blueprint(annotations)

if app.config.get("VOWEL_COMMERCIAL"):
app.register_blueprint(landing)


Loading…
Cancel
Save