From dfbe01bdd67d2d1835f51cf74bd1ff790789bd63 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Fri, 17 Jul 2020 13:35:09 +0200 Subject: [PATCH] event organizer --- app.py | 27 ++++++++++-- forms/event.py | 27 ++++++++++-- jsonld.py | 62 +++++++++++++++++++++++++++- migrations/versions/975c22ae802b_.py | 59 ++++++++++++++++++++++++++ models.py | 20 +++++++-- templates/_macros.html | 30 ++++++++++++++ templates/event/create.html | 14 +++++++ templates/event/update.html | 14 +++++++ 8 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 migrations/versions/975c22ae802b_.py diff --git a/app.py b/app.py index 7a28331..2c20362 100644 --- a/app.py +++ b/app.py @@ -53,7 +53,7 @@ app.json_encoder = DateTimeEncoder # Setup Flask-Security # Define models -from models import EventCategory, Image, EventSuggestion, EventSuggestionDate, OrgOrAdminUnit, Actor, Place, Location, User, Role, AdminUnit, AdminUnitMember, AdminUnitMemberRole, OrgMember, OrgMemberRole, Organization, AdminUnitOrg, AdminUnitOrgRole, Event, EventDate +from models import EventOrganizer, EventCategory, Image, EventSuggestion, EventSuggestionDate, OrgOrAdminUnit, Actor, Place, Location, User, Role, AdminUnit, AdminUnitMember, AdminUnitMemberRole, OrgMember, OrgMemberRole, Organization, AdminUnitOrg, AdminUnitOrgRole, Event, EventDate user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role) security = Security(app, user_datastore) from oauth import blueprint @@ -715,6 +715,20 @@ def get_event_suggestions_for_current_user(): @app.before_first_request def create_initial_data(): + events = Event.query.filter_by(organizer = None).all() + for event in events: + if event.host: + ooa = event.host.admin_unit if event.host.admin_unit else event.host.organization + + organizer = EventOrganizer() + organizer.org_name = ooa.name + organizer.url = ooa.url + organizer.email = ooa.email + organizer.phone = ooa.phone + + event.organizer = organizer + + db.session.commit() return # Event categories @@ -1293,6 +1307,9 @@ from forms.admin_unit import CreateAdminUnitForm, UpdateAdminUnitForm def update_event_with_form(event, form): form.populate_obj(event) + if event.host_id == 0: + event.host_id = None + update_event_dates_with_recurrence_rule(event, form.start.data, form.end.data) if form.photo_file.data: @@ -1305,6 +1322,9 @@ def prepare_event_form(form): form.category_id.choices = sorted([(c.id, get_event_category_name(c)) for c in EventCategory.query.all()], key=lambda ooa: ooa[1]) form.admin_unit_id.choices = sorted([(admin_unit.id, admin_unit.name) for admin_unit in get_admin_units_for_organizations()], key=lambda admin_unit: admin_unit[1]) + form.host_id.choices.insert(0, (0, '')) + form.place_id.choices.insert(0, (0, '')) + @app.route("/events/create", methods=('GET', 'POST')) @auth_required() def event_create(): @@ -1313,11 +1333,10 @@ def event_create(): form = CreateEventForm(category_id=upsert_event_category('Other').id) prepare_event_form(form) - form.host_id.choices.insert(0, (0, '')) - form.place_id.choices.insert(0, (0, '')) if form.validate_on_submit(): event = Event() + event.organizer = EventOrganizer() update_event_with_form(event, form) try: @@ -1327,6 +1346,8 @@ def event_create(): return redirect(url_for('event', event_id=event.id)) except SQLAlchemyError as e: flash(handleSqlError(e), 'danger') + else: + flash_errors(form) return render_template('event/create.html', form=form) @app.route('/event//update', methods=('GET', 'POST')) diff --git a/forms/event.py b/forms/event.py index 84936cc..f6fb583 100644 --- a/forms/event.py +++ b/forms/event.py @@ -1,13 +1,30 @@ -from flask_babelex import lazy_gettext +from flask_babelex import lazy_gettext, gettext from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed -from wtforms import DateTimeField, StringField, SubmitField, TextAreaField, SelectField, BooleanField, IntegerField -from wtforms.fields.html5 import DateTimeLocalField +from wtforms import DateTimeField, StringField, SubmitField, TextAreaField, SelectField, BooleanField, IntegerField, FormField +from wtforms.fields.html5 import DateTimeLocalField, EmailField from wtforms.validators import DataRequired, Optional from wtforms.widgets import html_params, HTMLString from models import EventTargetGroupOrigin, EventAttendanceMode, EventStatus from .widgets import CustomDateTimeField +class EventOrganizerForm(FlaskForm): + name = StringField(lazy_gettext('Name'), validators=[Optional()]) + org_name = StringField(lazy_gettext('Organization'), validators=[Optional()]) + url = StringField(lazy_gettext('Link URL'), validators=[Optional()]) + email = EmailField(lazy_gettext('Email'), validators=[Optional()]) + phone = StringField(lazy_gettext('Phone'), validators=[Optional()]) + + def validate(self): + if not super(EventOrganizerForm, self).validate(): + return False + if not self.name.data and not self.org_name.data: + msg = gettext('At least one of name and organization must be set') + self.name.errors.append(msg) + self.org_name.errors.append(msg) + return False + return True + class CreateEventForm(FlaskForm): submit = SubmitField(lazy_gettext("Create event")) name = StringField(lazy_gettext('Name'), validators=[DataRequired()]) @@ -20,8 +37,10 @@ class CreateEventForm(FlaskForm): previous_start_date = CustomDateTimeField(lazy_gettext('Previous start date'), validators=[Optional()]) tags = StringField(lazy_gettext('Tags'), validators=[Optional()]) + organizer = FormField(EventOrganizerForm) + place_id = SelectField(lazy_gettext('Place'), validators=[DataRequired()], coerce=int) - host_id = SelectField(lazy_gettext('Host'), validators=[DataRequired()], coerce=int) + host_id = SelectField(lazy_gettext('Host'), validators=[Optional()], coerce=int) category_id = SelectField(lazy_gettext('Category'), validators=[DataRequired()], coerce=int) admin_unit_id = SelectField(lazy_gettext('Admin unit'), validators=[DataRequired()], coerce=int) diff --git a/jsonld.py b/jsonld.py index dee41aa..a00538b 100644 --- a/jsonld.py +++ b/jsonld.py @@ -39,6 +39,60 @@ def get_sd_for_admin_unit(admin_unit): result["name"] = admin_unit.name return result +def get_sd_for_organizer_organization_contact(organizer): + result = {} + result["@type"] = "ContactPoint" + result["name"] = organizer.name + + if organizer.email: + result["email"] = organizer.email + + if organizer.phone: + result["telephone"] = organizer.phone + + return result + +def get_sd_for_organizer_organization(organizer): + result = {} + result["@type"] = "Organization" + result["name"] = organizer.org_name + + if organizer.name: + result["contactPoint"] = get_sd_for_organizer_organization_contact(organizer) + else: + if organizer.email: + result["email"] = organizer.email + + if organizer.phone: + result["phone"] = organizer.phone + + if organizer.url: + result["url"] = organizer.url + + return result + +def get_sd_for_organizer_person(organizer): + result = {} + result["@type"] = "Person" + result["name"] = organizer.name + + if organizer.email: + result["email"] = organizer.email + + if organizer.phone: + result["phone"] = organizer.phone + + if organizer.url: + result["url"] = organizer.url + + return result + +def get_sd_for_organizer(organizer): + if organizer.org_name: + return get_sd_for_organizer_organization(organizer) + + return get_sd_for_organizer_person(organizer) + def get_sd_for_ooa(ooa): if ooa.organization: return get_sd_for_org(ooa.organization) @@ -96,7 +150,13 @@ def get_sd_for_event_date(event_date): result["description"] = event.description result["startDate"] = event_date.start result["location"] = get_sd_for_place(event.place) - result["organizer"] = get_sd_for_ooa(event.host) + + organizer_list = list() + if event.organizer: + organizer_list.append(get_sd_for_organizer(event.organizer)) + if event.host: + organizer_list.append(get_sd_for_ooa(event.host)) + result["organizer"] = organizer_list if event_date.end: result["endDate"] = event_date.end diff --git a/migrations/versions/975c22ae802b_.py b/migrations/versions/975c22ae802b_.py new file mode 100644 index 0000000..db5a7b0 --- /dev/null +++ b/migrations/versions/975c22ae802b_.py @@ -0,0 +1,59 @@ +"""empty message + +Revision ID: 975c22ae802b +Revises: 5c8457f2eac1 +Create Date: 2020-07-17 11:27:53.084732 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +import db + + +# revision identifiers, used by Alembic. +revision = '975c22ae802b' +down_revision = '5c8457f2eac1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('eventorganizer', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.Unicode(length=255), nullable=True), + sa.Column('org_name', sa.Unicode(length=255), nullable=True), + sa.Column('url', sa.String(length=255), nullable=True), + sa.Column('email', sa.Unicode(length=255), nullable=True), + sa.Column('phone', sa.Unicode(length=255), nullable=True), + sa.Column('created_by_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['created_by_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('event', sa.Column('organizer_id', sa.Integer(), nullable=True)) + op.alter_column('event', 'host_id', + existing_type=sa.INTEGER(), + nullable=True) + op.alter_column('event', 'place_id', + existing_type=sa.INTEGER(), + nullable=True) + op.create_foreign_key(None, 'event', 'eventorganizer', ['organizer_id'], ['id']) + op.drop_constraint('place_name_key', 'place', type_='unique') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('place_name_key', 'place', ['name']) + op.drop_constraint(None, 'event', type_='foreignkey') + op.alter_column('event', 'place_id', + existing_type=sa.INTEGER(), + nullable=False) + op.alter_column('event', 'host_id', + existing_type=sa.INTEGER(), + nullable=False) + op.drop_column('event', 'organizer_id') + op.drop_table('eventorganizer') + # ### end Alembic commands ### diff --git a/models.py b/models.py index 6988ad5..74aa1a9 100644 --- a/models.py +++ b/models.py @@ -205,7 +205,7 @@ class Location(db.Model, TrackableMixin): class Place(db.Model, TrackableMixin): __tablename__ = 'place' id = Column(Integer(), primary_key=True) - name = Column(Unicode(255), nullable=False, unique=True) + name = Column(Unicode(255), nullable=False) location_id = db.Column(db.Integer, db.ForeignKey('location.id')) location = db.relationship('Location') photo_id = db.Column(db.Integer, db.ForeignKey('image.id')) @@ -260,14 +260,28 @@ class EventStatus(IntEnum): postponed = 4 rescheduled = 5 +class EventOrganizer(db.Model, TrackableMixin): + __tablename__ = 'eventorganizer' + __table_args__ = ( + CheckConstraint('NOT(name IS NULL AND org_name IS NULL)'), + ) + id = Column(Integer(), primary_key=True) + name = Column(Unicode(255)) + org_name = Column(Unicode(255)) + url = Column(String(255)) + email = Column(Unicode(255)) + phone = Column(Unicode(255)) + class Event(db.Model, TrackableMixin): __tablename__ = 'event' id = Column(Integer(), primary_key=True) admin_unit_id = db.Column(db.Integer, db.ForeignKey('adminunit.id'), nullable=False) admin_unit = db.relationship('AdminUnit', backref=db.backref('events', lazy=True)) - host_id = db.Column(db.Integer, db.ForeignKey('org_or_adminunit.id'), nullable=False) + organizer_id = db.Column(db.Integer, db.ForeignKey('eventorganizer.id'), nullable=True) + organizer = db.relationship('EventOrganizer', uselist=False) + host_id = db.Column(db.Integer, db.ForeignKey('org_or_adminunit.id'), nullable=True) host = db.relationship('OrgOrAdminUnit', backref=db.backref('events', lazy=True)) - place_id = db.Column(db.Integer, db.ForeignKey('place.id'), nullable=False) + place_id = db.Column(db.Integer, db.ForeignKey('place.id'), nullable=True) place = db.relationship('Place', backref=db.backref('events', lazy=True)) name = Column(Unicode(255), nullable=False) description = Column(UnicodeText(), nullable=False) diff --git a/templates/_macros.html b/templates/_macros.html index fb630de..892c71f 100644 --- a/templates/_macros.html +++ b/templates/_macros.html @@ -155,6 +155,15 @@ {% endif %} {% endmacro %} +{% macro render_string_prop(prop, icon = None, label_key = None) %} +{% if prop %} +
+ {% if icon %}{% endif %} + {{ prop }} +
+{% endif %} +{% endmacro %} + {% macro render_bool_prop(prop, icon, label_key) %} {% if prop %}
@@ -319,6 +328,25 @@
+ {% if event.organizer %} + +
+
+ {{ _('Organizer') }} +
+
+ {{ render_string_prop(event.organizer.name) }} + {{ render_string_prop(event.organizer.org_name) }} + {{ render_link_prop(event.organizer.url) }} + {{ render_email_prop(event.organizer.email) }} + {{ render_phone_prop(event.organizer.phone) }} +
+
+ + {% endif %} + + {% if event.host %} +
{{ _('Host') }} @@ -359,6 +387,8 @@
+ + {% endif %} {% endmacro %} {% macro render_google_sign_in_button() %} diff --git a/templates/event/create.html b/templates/event/create.html index 8193e14..48f4f23 100644 --- a/templates/event/create.html +++ b/templates/event/create.html @@ -48,6 +48,20 @@ +
+
+ {{ _('Organizer') }} +
+
+ {{ form.organizer.hidden_tag() }} + {{ render_field_with_errors(form.organizer.form.name) }} + {{ render_field_with_errors(form.organizer.form.org_name) }} + {{ render_field_with_errors(form.organizer.form.url) }} + {{ render_field_with_errors(form.organizer.form.email) }} + {{ render_field_with_errors(form.organizer.form.phone) }} +
+
+
{{ _('Place') }} diff --git a/templates/event/update.html b/templates/event/update.html index 551104a..e2ac2ea 100644 --- a/templates/event/update.html +++ b/templates/event/update.html @@ -48,6 +48,20 @@
+
+
+ {{ _('Organizer') }} +
+
+ {{ form.organizer.hidden_tag() }} + {{ render_field_with_errors(form.organizer.form.name) }} + {{ render_field_with_errors(form.organizer.form.org_name) }} + {{ render_field_with_errors(form.organizer.form.url) }} + {{ render_field_with_errors(form.organizer.form.email) }} + {{ render_field_with_errors(form.organizer.form.phone) }} +
+
+
{{ _('Place') }}