diff --git a/app.py b/app.py index 7547abb..89b2a2d 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,6 @@ import pytz import json from urllib.parse import quote_plus from dateutil.rrule import rrulestr, rruleset, rrule -from jsonld import DateTimeEncoder, get_sd_for_event_date # Create app app = Flask(__name__) @@ -41,11 +40,12 @@ babel = Babel(app) app.jinja_env.filters['quote_plus'] = lambda u: quote_plus(u) -app.json_encoder = DateTimeEncoder - # create db db = SQLAlchemy(app) +from jsonld import DateTimeEncoder, get_sd_for_event_date +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 @@ -68,6 +68,11 @@ def get_event_category_name(category): app.jinja_env.filters['event_category_name'] = lambda u: get_event_category_name(u) +def get_localized_enum_name(enum): + return lazy_gettext(enum.__class__.__name__ + '.' + enum.name) + +app.jinja_env.filters['loc_enum'] = lambda u: get_localized_enum_name(u) + def print_dynamic_texts(): gettext('Event_Art') gettext('Event_Book') @@ -87,6 +92,7 @@ def print_dynamic_texts(): gettext('Event_Fitness') gettext('Event_Sports') gettext('Event_Other') + gettext('Typical Age range') def handleSqlError(e): message = str(e.__dict__['orig']) @@ -951,6 +957,14 @@ def create_initial_data(): db.session.commit() +def flash_errors(form): + for field, errors in form.errors.items(): + for error in errors: + flash(gettext("Error in the %s field - %s") % ( + getattr(form, field).label.text, + error + ), 'danger') + # Views @app.route("/") def home(): @@ -1228,7 +1242,7 @@ from forms.admin_unit import CreateAdminUnitForm, UpdateAdminUnitForm def update_event_with_form(event, form): form.populate_obj(event) - eventDate = EventDate(event_id = event.id, start=form.start.data) + eventDate = EventDate(event_id = event.id, start=form.start.data, end=form.end.data) event.dates = [eventDate] if form.photo_file.data: @@ -1272,7 +1286,7 @@ def event_update(event_id): if not can_update_event(event): abort(401) - form = UpdateEventForm(obj=event,start=event.dates[0].start) + form = UpdateEventForm(obj=event,start=event.dates[0].start,end=event.dates[0].end) prepare_event_form(form) if form.validate_on_submit(): @@ -1284,6 +1298,8 @@ def event_update(event_id): 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/update.html', form=form, diff --git a/babel.cfg b/babel.cfg index 63160f3..3fa3dee 100644 --- a/babel.cfg +++ b/babel.cfg @@ -1,3 +1,4 @@ -[python: *.py] +[ignore: env/**] +[python: **.py] [jinja2: templates/**.html] extensions=jinja2.ext.autoescape,jinja2.ext.with_ \ No newline at end of file diff --git a/db.py b/db.py new file mode 100644 index 0000000..536092c --- /dev/null +++ b/db.py @@ -0,0 +1,16 @@ +from sqlalchemy.types import TypeDecorator +from sqlalchemy import Integer + +class IntegerEnum(TypeDecorator): + impl = Integer + def __init__(self, enumtype, *args, **kwargs): + super().__init__(*args, **kwargs) + self._enumtype = enumtype + + def process_bind_param(self, value, dialect): + return value + + def process_result_value(self, value, dialect): + if value: + return self._enumtype(value) + return None diff --git a/forms/event.py b/forms/event.py index 382d2a7..581dae4 100644 --- a/forms/event.py +++ b/forms/event.py @@ -1,9 +1,10 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed -from wtforms import StringField, SubmitField, TextAreaField, SelectField +from wtforms import StringField, SubmitField, TextAreaField, SelectField, BooleanField, IntegerField from wtforms.fields.html5 import DateTimeLocalField from wtforms.validators import DataRequired, Optional +from models import EventTargetGroupOrigin, EventAttendanceMode, EventStatus class CreateEventForm(FlaskForm): submit = SubmitField(lazy_gettext("Create event")) @@ -12,12 +13,37 @@ class CreateEventForm(FlaskForm): ticket_link = StringField(lazy_gettext('Ticket Link URL'), validators=[Optional()]) description = TextAreaField(lazy_gettext('Description'), validators=[DataRequired()]) start = DateTimeLocalField(lazy_gettext('Start'), format='%Y-%m-%dT%H:%M', validators=[DataRequired()]) + end = DateTimeLocalField(lazy_gettext('End'), format='%Y-%m-%dT%H:%M', validators=[Optional()]) + previous_start_date = DateTimeLocalField(lazy_gettext('Previous start date'), format='%Y-%m-%dT%H:%M', validators=[Optional()]) + tags = StringField(lazy_gettext('Tags'), validators=[Optional()]) place_id = SelectField(lazy_gettext('Place'), validators=[DataRequired()], coerce=int) host_id = SelectField(lazy_gettext('Host'), validators=[DataRequired()], coerce=int) category_id = SelectField(lazy_gettext('Category'), validators=[DataRequired()], coerce=int) admin_unit_id = SelectField(lazy_gettext('Admin unit'), validators=[DataRequired()], coerce=int) + kid_friendly = BooleanField(lazy_gettext('Kid friendly'), validators=[Optional()]) + accessible_for_free = BooleanField(lazy_gettext('Accessible for free'), validators=[Optional()]) + age_from = IntegerField(lazy_gettext('Typical Age from'), validators=[Optional()]) + age_to = IntegerField(lazy_gettext('Typical Age to'), validators=[Optional()]) + + target_group_origin = SelectField(lazy_gettext('Target group origin'), coerce=int, choices=[ + (int(EventTargetGroupOrigin.both), lazy_gettext('EventTargetGroupOrigin.both')), + (int(EventTargetGroupOrigin.tourist), lazy_gettext('EventTargetGroupOrigin.tourist')), + (int(EventTargetGroupOrigin.resident), lazy_gettext('EventTargetGroupOrigin.resident'))]) + + attendance_mode = SelectField(lazy_gettext('Attendance mode'), coerce=int, choices=[ + (int(EventAttendanceMode.offline), lazy_gettext('EventAttendanceMode.offline')), + (int(EventAttendanceMode.online), lazy_gettext('EventAttendanceMode.online')), + (int(EventAttendanceMode.mixed), lazy_gettext('EventAttendanceMode.mixed'))]) + + status = SelectField(lazy_gettext('Status'), coerce=int, choices=[ + (int(EventStatus.scheduled), lazy_gettext('EventStatus.scheduled')), + (int(EventStatus.cancelled), lazy_gettext('EventStatus.cancelled')), + (int(EventStatus.movedOnline), lazy_gettext('EventStatus.movedOnline')), + (int(EventStatus.postponed), lazy_gettext('EventStatus.postponed')), + (int(EventStatus.rescheduled), lazy_gettext('EventStatus.rescheduled'))]) + photo_file = FileField(lazy_gettext('Photo'), validators=[FileAllowed(['jpg', 'jpeg', 'png'], lazy_gettext('Images only!'))]) class UpdateEventForm(CreateEventForm): diff --git a/jsonld.py b/jsonld.py index 851fcd8..dee41aa 100644 --- a/jsonld.py +++ b/jsonld.py @@ -2,6 +2,7 @@ import datetime import decimal from json import JSONEncoder from flask import url_for +from models import EventAttendanceMode, EventStatus # subclass JSONEncoder class DateTimeEncoder(JSONEncoder): @@ -97,6 +98,38 @@ def get_sd_for_event_date(event_date): result["location"] = get_sd_for_place(event.place) result["organizer"] = get_sd_for_ooa(event.host) + if event_date.end: + result["endDate"] = event_date.end + + if event.previous_start_date: + result["previousStartDate"] = event.previous_start_date + + if event.accessible_for_free: + result["accessible_for_free"] = event.accessible_for_free + + if event.age_from or event.age_to: + result["typicalAgeRange"] = "%d-%d" % (event.age_from, event.age_to) + + if event.attendance_mode: + if event.attendance_mode == EventAttendanceMode.offline: + result["eventAttendanceMode"] = "OfflineEventAttendanceMode" + elif event.attendance_mode == EventAttendanceMode.online: + result["eventAttendanceMode"] = "OnlineEventAttendanceMode" + elif event.attendance_mode == EventAttendanceMode.mixed: + result["eventAttendanceMode"] = "MixedEventAttendanceMode" + + if event.status: + if event.status == EventStatus.scheduled: + result["eventStatus"] = "EventScheduled" + elif event.status == EventStatus.cancelled: + result["eventStatus"] = "EventCancelled" + elif event.status == EventStatus.movedOnline: + result["eventStatus"] = "EventMovedOnline" + elif event.status == EventStatus.postponed: + result["eventStatus"] = "EventPostponed" + elif event.status == EventStatus.rescheduled: + result["eventStatus"] = "EventRescheduled" + if event.photo_id: result["image"] = url_for('image', id=event.photo_id) diff --git a/migrations/script.py.mako b/migrations/script.py.mako index ee5cea7..136acbc 100644 --- a/migrations/script.py.mako +++ b/migrations/script.py.mako @@ -8,6 +8,7 @@ Create Date: ${create_date} from alembic import op import sqlalchemy as sa import sqlalchemy_utils +import db ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/migrations/versions/ed6bb2084bbd_.py b/migrations/versions/ed6bb2084bbd_.py new file mode 100644 index 0000000..82fdb0c --- /dev/null +++ b/migrations/versions/ed6bb2084bbd_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: ed6bb2084bbd +Revises: f1bc3fa623c7 +Create Date: 2020-07-08 08:53:44.373606 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +import db + + +# revision identifiers, used by Alembic. +revision = 'ed6bb2084bbd' +down_revision = 'f1bc3fa623c7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event', sa.Column('previous_start_date', sa.DateTime(timezone=True), nullable=True)) + op.add_column('eventdate', sa.Column('end', sa.DateTime(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('eventdate', 'end') + op.drop_column('event', 'previous_start_date') + # ### end Alembic commands ### diff --git a/migrations/versions/f1bc3fa623c7_.py b/migrations/versions/f1bc3fa623c7_.py new file mode 100644 index 0000000..f536f7f --- /dev/null +++ b/migrations/versions/f1bc3fa623c7_.py @@ -0,0 +1,44 @@ +"""empty message + +Revision ID: f1bc3fa623c7 +Revises: 75c07cb9cfe3 +Create Date: 2020-07-07 15:49:58.653888 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +import db +from models import EventTargetGroupOrigin, EventAttendanceMode, EventStatus + +# revision identifiers, used by Alembic. +revision = 'f1bc3fa623c7' +down_revision = '75c07cb9cfe3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event', sa.Column('accessible_for_free', sa.Boolean(), nullable=True)) + op.add_column('event', sa.Column('age_from', sa.Integer(), nullable=True)) + op.add_column('event', sa.Column('age_to', sa.Integer(), nullable=True)) + op.add_column('event', sa.Column('attendance_mode', db.IntegerEnum(EventAttendanceMode), nullable=True)) + op.add_column('event', sa.Column('kid_friendly', sa.Boolean(), nullable=True)) + op.add_column('event', sa.Column('status', db.IntegerEnum(EventStatus), nullable=True)) + op.add_column('event', sa.Column('tags', sa.UnicodeText(), nullable=True)) + op.add_column('event', sa.Column('target_group_origin', db.IntegerEnum(EventTargetGroupOrigin), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('event', 'target_group_origin') + op.drop_column('event', 'tags') + op.drop_column('event', 'status') + op.drop_column('event', 'kid_friendly') + op.drop_column('event', 'attendance_mode') + op.drop_column('event', 'age_to') + op.drop_column('event', 'age_from') + op.drop_column('event', 'accessible_for_free') + # ### end Alembic commands ### diff --git a/models.py b/models.py index 8253d4b..1869951 100644 --- a/models.py +++ b/models.py @@ -2,10 +2,13 @@ from app import db from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import relationship, backref from sqlalchemy.schema import CheckConstraint +from sqlalchemy.types import TypeDecorator from sqlalchemy import UniqueConstraint, Boolean, DateTime, Column, Integer, String, ForeignKey, Unicode, UnicodeText, Numeric, LargeBinary from flask_security import UserMixin, RoleMixin from flask_dance.consumer.storage.sqla import OAuthConsumerMixin +from enum import IntEnum import datetime +from db import IntegerEnum ### Base @@ -240,6 +243,23 @@ class EventSuggestionDate(db.Model): start = db.Column(db.DateTime(timezone=True), nullable=False) #end: date_time +class EventTargetGroupOrigin(IntEnum): + both = 1 + tourist = 2 + resident = 3 + +class EventAttendanceMode(IntEnum): + offline = 1 + online = 2 + mixed = 3 + +class EventStatus(IntEnum): + scheduled = 1 + cancelled = 2 + movedOnline = 3 + postponed = 4 + rescheduled = 5 + class Event(db.Model, TrackableMixin): __tablename__ = 'event' id = Column(Integer(), primary_key=True) @@ -258,6 +278,15 @@ class Event(db.Model, TrackableMixin): photo = db.relationship('Image', uselist=False) category_id = db.Column(db.Integer, db.ForeignKey('eventcategory.id'), nullable=False) category = relationship('EventCategory', uselist=False) + tags = Column(UnicodeText()) + kid_friendly = Column(Boolean()) + accessible_for_free = Column(Boolean()) + age_from = Column(Integer()) + age_to = Column(Integer()) + target_group_origin = Column(IntegerEnum(EventTargetGroupOrigin)) + attendance_mode = Column(IntegerEnum(EventAttendanceMode)) + status = Column(IntegerEnum(EventStatus)) + previous_start_date = db.Column(db.DateTime(timezone=True), nullable=True) recurrence_rule = Column(UnicodeText()) dates = relationship('EventDate', backref=backref('event', lazy=False), cascade="all, delete-orphan") @@ -267,4 +296,4 @@ class EventDate(db.Model): id = Column(Integer(), primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) start = db.Column(db.DateTime(timezone=True), nullable=False) - #end: date_time \ No newline at end of file + end = db.Column(db.DateTime(timezone=True), nullable=True) diff --git a/templates/_macros.html b/templates/_macros.html index c14e43d..2f01795 100644 --- a/templates/_macros.html +++ b/templates/_macros.html @@ -145,6 +145,42 @@ {% endif %} {% endmacro %} +{% macro render_bool_prop(prop, icon, label_key) %} +{% if prop %} +