From a151f3a22abe228beea91cb4896f2e8c7d181a62 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 14 Jun 2020 19:46:37 +0200 Subject: [PATCH] meilenstein --- README.md | 11 + app.py | 369 ++++++++++++++++++++++++++- forms/__init__.py | 0 forms/create_event.py | 13 + manage.py | 2 +- migrations/versions/a2ba246b23d0_.py | 142 +++++++++++ models.py | 119 ++++++++- static/site.css | 40 ++- templates/admin/admin.html | 5 +- templates/admin/admin_units.html | 16 ++ templates/admin_unit.html | 58 +++++ templates/admin_units.html | 26 ++ templates/create_event.html | 18 ++ templates/event.html | 44 ++++ templates/events.html | 41 +++ templates/home.html | 54 +++- templates/layout.html | 17 +- templates/organization.html | 38 +++ templates/organizations.html | 26 ++ templates/profile.html | 46 +++- 20 files changed, 1059 insertions(+), 26 deletions(-) create mode 100644 forms/__init__.py create mode 100644 forms/create_event.py create mode 100644 migrations/versions/a2ba246b23d0_.py create mode 100644 templates/admin_unit.html create mode 100644 templates/admin_units.html create mode 100644 templates/create_event.html create mode 100644 templates/event.html create mode 100644 templates/events.html create mode 100644 templates/organization.html create mode 100644 templates/organizations.html diff --git a/README.md b/README.md index 7c84e5f..00cba05 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,17 @@ python manage.py db init python manage.py db migrate python manage.py db upgrade +## Developemt !!! +python manage.py db history +python manage.py db downgrade +// reset git: migrations/versions +python manage.py db migrate +python manage.py db upgrade + +## Kill local detached server +lsof -i :5000 +kill -9 PIDNUMBER + # i18n https://pythonhosted.org/Flask-BabelEx/ diff --git a/app.py b/app.py index 8c5c71b..167a6fb 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,13 @@ import os -from flask import Flask, render_template, request +from flask import Flask, render_template, request, url_for, redirect from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import joinedload from flask_security import Security, current_user, auth_required, roles_required, hash_password, SQLAlchemySessionUserDatastore -from flask_babelex import Babel, gettext, lazy_gettext +from flask_security.utils import FsPermNeed +from flask_babelex import Babel, gettext, lazy_gettext, format_datetime +from flask_principal import Permission +from datetime import datetime +import pytz # Create app app = Flask(__name__) @@ -30,7 +35,7 @@ db = SQLAlchemy(app) # Setup Flask-Security # Define models -from models import User, Role +from models import User, Role, AdminUnit, AdminUnitMember, AdminUnitMemberRole, OrgMember, OrgMemberRole, Organization, AdminUnitOrg, AdminUnitOrgRole, Event, EventDate user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role) security = Security(app, user_datastore) @@ -39,32 +44,369 @@ def get_locale(): return request.accept_languages.best_match(app.config['LANGUAGES']) # Create a user to test with +def upsert_user(email, password="password"): + result = user_datastore.find_user(email=email) + if result is None: + result = user_datastore.create_user(email=email, password=hash_password(password)) + return result + +def upsert_admin_unit(unit_name): + admin_unit = AdminUnit.query.filter_by(name = unit_name).first() + if admin_unit is None: + admin_unit = AdminUnit(name = unit_name) + db.session.add(admin_unit) + return admin_unit + +def get_admin_unit(unit_name): + return AdminUnit.query.filter_by(name = unit_name).first() + +def upsert_org_member_role(role_name, permissions): + result = OrgMemberRole.query.filter_by(name = role_name).first() + if result is None: + result = OrgMemberRole(name = role_name) + result.remove_permissions(result.get_permissions()) + result.add_permissions(permissions) + db.session.add(result) + return result + +def upsert_admin_unit_member_role(role_name, permissions): + result = AdminUnitMemberRole.query.filter_by(name = role_name).first() + if result is None: + result = AdminUnitMemberRole(name = role_name) + db.session.add(result) + + result.remove_permissions(result.get_permissions()) + result.add_permissions(permissions) + return result + +def upsert_admin_unit_org_role(role_name, permissions): + result = AdminUnitOrgRole.query.filter_by(name = role_name).first() + if result is None: + result = AdminUnitOrgRole(name = role_name) + db.session.add(result) + + result.remove_permissions(result.get_permissions()) + result.add_permissions(permissions) + return result + +def add_user_to_organization(user, organization): + result = OrgMember.query.with_parent(organization).filter_by(user_id = user.id).first() + if result is None: + result = OrgMember(user = user, organization_id = organization.id) + organization.members.append(result) + db.session.add(result) + return result + +def add_user_to_admin_unit(user, admin_unit): + result = OrgMember.query.with_parent(admin_unit).filter_by(user_id = user.id).first() + if result is None: + result = OrgMember(user = user) + admin_unit.members.append(result) + db.session.add(result) + return result + +def add_user_to_admin_unit(user, admin_unit): + result = AdminUnitMember.query.with_parent(admin_unit).filter_by(user_id = user.id).first() + if result is None: + result = AdminUnitMember(user = user, admin_unit_id=admin_unit.id) + admin_unit.members.append(result) + db.session.add(result) + return result + +def add_organization_to_admin_unit(organization, admin_unit): + result = AdminUnitOrg.query.with_parent(admin_unit).filter_by(organization_id = organization.id).first() + if result is None: + result = AdminUnitOrg(organization = organization, admin_unit_id=admin_unit.id) + admin_unit.organizations.append(result) + db.session.add(result) + return result + +def add_role_to_admin_unit_org(admin_unit_org, role): + if AdminUnitOrgRole.query.with_parent(admin_unit_org).filter_by(name = role.name).first() is None: + admin_unit_org.roles.append(role) + +def add_role_to_admin_unit_member(admin_unit_member, role): + if AdminUnitMemberRole.query.with_parent(admin_unit_member).filter_by(name = role.name).first() is None: + admin_unit_member.roles.append(role) + +def add_role_to_org_member(org_member, role): + if OrgMemberRole.query.with_parent(org_member).filter_by(name = role.name).first() is None: + org_member.roles.append(role) + +def upsert_organization(org_name): + result = Organization.query.filter_by(name = org_name).first() + if result is None: + result = Organization(name = org_name) + db.session.add(result) + return result + +def create_berlin_date(year, month, day, hour, minute = 0): + return pytz.timezone('Europe/Berlin').localize(datetime(year, month, day, hour=hour, minute=minute)) + +def upsert_event(event_name, host, location, start, description, link = None, verified = False): + result = Event().query.filter_by(name = event_name).first() + if result is None: + result = Event() + result.name = event_name + result.host = host + result.location = location + result.description = description + result.external_link = link + result.admin_unit = get_admin_unit('Stadt Goslar') + result.verified = verified + eventDate = EventDate(event_id = result.id, start=start) + result.dates.append(eventDate) + db.session.add(result) + return result + +def has_admin_unit_member_permission(admin_unit_id, permission): + admin_unit_member = AdminUnitMember.query.filter_by(admin_unit_id=admin_unit_id, user_id=current_user.id).first() + if admin_unit_member is not None: + for role in admin_unit_member.roles: + if permission in role.get_permissions(): + return True + + return False + +def can_list_admin_unit_members(admin_unit): + if not current_user.is_authenticated: + return False + + # User permission, e.g. user is global admin + user_perm = Permission(FsPermNeed('admin_unit.members:read')) + if user_perm.can(): + return True + + if has_admin_unit_member_permission(admin_unit.id, 'admin_unit.members:read'): + return True + + return False + +def can_list_org_members(organization): + if not current_user.is_authenticated: + return False + + # User permission, e.g. user is global admin + user_perm = Permission(FsPermNeed('organization.members:read')) + if user_perm.can(): + return True + + return True # todo + +def can_verify_event(event): + if not current_user.is_authenticated: + return False + + # User permission, e.g. user is global admin + user_perm = Permission(FsPermNeed('event:verify')) + if user_perm.can(): + return True + + # Admin unit member permissions (Holger, Artur) + if has_admin_unit_member_permission(event.admin_unit_id, 'event:verify'): + return True + + # Event has Admin Unit + # Admin Unit has organization members with roles with permission 'event:verify' + # This organization has members with roles with permission 'event:verify' + # Der aktuelle nutzer muss unter diesen nutzern sein + admin_unit_orgs = AdminUnitOrg.query.filter_by(admin_unit_id=event.admin_unit_id).all() + for admin_unit_org in admin_unit_orgs: + for admin_unit_org_role in admin_unit_org.roles: + if 'event:verify' in admin_unit_org_role.get_permissions(): + org_member = OrgMember.query.filter_by(organization_id=admin_unit_org.organization_id, user_id=current_user.id).first() + if org_member is not None: + for org_member_role in org_member.roles: + if 'event:verify' in org_member_role.get_permissions(): + return True + + return False + @app.before_first_request def create_user(): - admin_role = user_datastore.find_or_create_role("admin", permissions=["user_create"]) + # Admin units + goslar = upsert_admin_unit('Stadt Goslar') + upsert_admin_unit('Bad Harzburg') + upsert_admin_unit('Clausthal') + upsert_admin_unit('Walkenried') + upsert_admin_unit('Bad Lauterberg') + upsert_admin_unit('Harzgerode') + upsert_admin_unit('Ilsenburg') + upsert_admin_unit('Osterode') + upsert_admin_unit('Quedlinburg') + upsert_admin_unit('Wernigerode') + upsert_admin_unit('Halberstadt') + upsert_admin_unit('Wennigsen') + upsert_admin_unit('Hildesheim') - admin_user = user_datastore.find_user(email="grams.daniel@gmail.com") - if admin_user is None: - admin_user = user_datastore.create_user(email="grams.daniel@gmail.com", password=hash_password("password")) + # Organizations + admin_unit_org_event_verifier_role = upsert_admin_unit_org_role('event_verifier', ['event:verify']) + gmg = upsert_organization("GOSLAR marketing gmbh") + gz = upsert_organization("Goslarsche Zeitung") - user_datastore.add_role_to_user(admin_user, admin_role) + gmg_admin_unit_org = add_organization_to_admin_unit(gmg, goslar) + add_role_to_admin_unit_org(gmg_admin_unit_org, admin_unit_org_event_verifier_role) - normal_user = user_datastore.find_user(email="normal.user@gmail.com") - if normal_user is None: - normal_user = user_datastore.create_user(email="normal.user@gmail.com", password=hash_password("password")) + gz_admin_unit_org = add_organization_to_admin_unit(gz, goslar) + add_role_to_admin_unit_org(gz_admin_unit_org, admin_unit_org_event_verifier_role) + + # Users + admin_role = user_datastore.find_or_create_role("admin") + admin_role.add_permissions(["user:create", "event:verify", "admin_unit.members:read", "organization.members:read"]) + + admin_unit_admin_role = upsert_admin_unit_member_role('admin', ["admin_unit.members:read"]) + admin_unit_event_verifier_role = upsert_admin_unit_member_role('event_verifier', ["event:verify"]) + org_member_event_verifier_role = upsert_org_member_role('event_verifier', ["event:verify"]) + + daniel = upsert_user("grams.daniel@gmail.com") + user_datastore.add_role_to_user(daniel, admin_role) + + holger = upsert_user("holger@test.de") + holger_goslar_member = add_user_to_admin_unit(holger, goslar) + add_role_to_admin_unit_member(holger_goslar_member, admin_unit_admin_role) + add_role_to_admin_unit_member(holger_goslar_member, admin_unit_event_verifier_role) + + artur = upsert_user("artur@test.de") + artur_goslar_member = add_user_to_admin_unit(artur, goslar) + add_role_to_admin_unit_member(artur_goslar_member, admin_unit_event_verifier_role) + + mia = upsert_user("mia@test.de") + mia_gmg_member = add_user_to_organization(mia, gmg) + add_role_to_org_member(mia_gmg_member, org_member_event_verifier_role) + + tom = upsert_user("tom@test.de") + tom_gz_member = add_user_to_organization(tom, gz) + add_role_to_org_member(tom_gz_member, org_member_event_verifier_role) + + # Events + berlin = pytz.timezone('Europe/Berlin') + upsert_event("Vienenburger Seefest", + "Stadt Goslar", + "Vienenburger See", + create_berlin_date(2020, 8, 14, 17, 0), + 'Vom 14. bis 16. August 2020 findet im ausgewiesenen Naherholungsgebiet am Fuße des Harzes, dem Goslarer Ortsteil Vienenburg, das Seefest unter dem Motto „Feuer & Wasser“ statt.', + 'https://www.goslar.de/kultur-freizeit/veranstaltungen/156-vienenburger-seefest?layout=*', + True) + + upsert_event("Tausend Schritte durch die Altstadt", + "Stadt Goslar", + "Tourist-Information Goslar", + create_berlin_date(2020, 9, 1, 10, 0), + 'Tausend Schritte durch die Altstadt Erleben Sie einen geführten Stadtrundgang durch den historischen Stadtkern. Lassen Sie sich von Fachwerkromantik und kaiserlichen Bauten inmitten der UNESCO-Welterbestätte verzaubern. ganzjährig (außer 01.01.) täglich 10:00 Uhr Treffpunkt: Tourist-Information am Marktplatz (Dauer ca. 2 Std.) Erwachsene 8,00 Euro Inhaber Gastkarte Goslar/Kurkarte Hahnenklee 7,00 Euro Schüler/Studenten 6,00 Euro') + + upsert_event("Spaziergang am Nachmittag", + "Stadt Goslar", + "Tourist-Information Goslar", + create_berlin_date(2020, 9, 1, 13, 30), + 'Spaziergang am Nachmittag Begeben Sie sich auf einen geführten Rundgang durch die historische Altstadt. Entdecken Sie malerische Fachwerkgassen und imposante Bauwerke bei einem Streifzug durch das UNESCO-Weltkulturerbe. April – Oktober und 25.11. – 30.12. Montag – Samstag 13:30 Uhr Treffpunkt: Tourist-Information am Marktplatz (Dauer ca. 1,5 Std.) Erwachsene 7,00 Euro Inhaber Gastkarte Goslar/Kurkarte Hahnenklee 6,00 Euro Schüler/Studenten 5,00 Euro') + + upsert_event("Ein Blick hinter die Kulissen - Rathausbaustelle", + "Stadt Goslar", + "Nagelkopf am Rathaus", + create_berlin_date(2020, 9, 3, 17, 0), + 'Allen interessierten Bürgern wird die Möglichkeit geboten, unter fachkundiger Führung des Goslarer Gebäudemanagement (GGM) einen Blick hinter die Kulissen durch das derzeit gesperrte historische Rathaus und die Baustelle Kulturmarktplatz zu werfen. Da bei beiden Führungen die Anzahl der Teilnehmer auf 16 Personen begrenzt ist, ist eine Anmeldung unbedingt notwendig sowie festes Schuhhwerk. Bitte melden Sie sich bei Interesse in der Tourist-Information (Tel. 05321-78060) an. Kinder unter 18 Jahren sind aus Sicherheitsgründen auf der Baustelle nicht zugelassen.') + + upsert_event("Wöltingerode unter Dampf", + "Kloster Wöltingerode", + "Kloster Wöltingerode", + create_berlin_date(2020, 9, 5, 12, 0), + 'Mit einem ländlichen Programm rund um historische Trecker und Landmaschinen, köstliche Produkte aus Brennerei und Region, einem Kunsthandwerkermarkt und besonderen Führungen findet das Hoffest auf dem Klostergut statt.', + 'https://www.woelti-unter-dampf.de') + + upsert_event("Altstadtfest", + "GOSLAR marketing gmbh", + "Goslar", + create_berlin_date(2020, 9, 11, 15, 0), + 'Drei Tage lang dürfen sich die Besucher des Goslarer Altstadtfestes auf ein unterhaltsames und abwechslungsreiches Veranstaltungsprogramm freuen. Der Flohmarkt auf der Kaiserpfalzwiese lädt zum Stöbern vor historischer Kulisse ein.', + 'https://www.goslar.de/kultur-freizeit/veranstaltungen/altstadtfest') + + upsert_event("Adventsmarkt auf der Vienenburg", + "Vienenburg", + "Vienenburg", + create_berlin_date(2020, 12, 13, 12, 0), + 'Inmitten der mittelalterlichen Burg mit dem restaurierten Burgfried findet der „Advent auf der Burg“ statt. Der Adventsmarkt wird von gemeinnützigen und sozialen Vereinen sowie den Kirchen ausgerichtet.') db.session.commit() # Views @app.route("/") def home(): - return render_template('home.html') + admin_unit_members = AdminUnitMember.query.filter_by(user_id = current_user.id).all() if current_user.is_authenticated else None + organization_members = OrgMember.query.filter_by(user_id = current_user.id).all() if current_user.is_authenticated else None + return render_template('home.html', + admin_unit_members=admin_unit_members, + organization_members=organization_members) + +@app.route("/admin_units") +def admin_units(): + return render_template('admin_units.html', + admin_units=AdminUnit.query.all()) + +@app.route('/admin_unit/') +def admin_unit(admin_unit_id): + admin_unit = AdminUnit.query.filter_by(id = admin_unit_id).first() + current_user_member = AdminUnitMember.query.with_parent(admin_unit).filter_by(user_id = current_user.id).first() if current_user.is_authenticated else None + + return render_template('admin_unit.html', + admin_unit=admin_unit, + current_user_member=current_user_member, + can_list_admin_unit_members=can_list_admin_unit_members(admin_unit)) + +@app.route("/organizations") +def organizations(): + return render_template('organizations.html', + organizations=Organization.query.all()) + +@app.route('/organization/') +def organization(organization_id): + organization = Organization.query.filter_by(id = organization_id).first() + current_user_member = OrgMember.query.with_parent(organization).filter_by(user_id = current_user.id).first() if current_user.is_authenticated else None + + return render_template('organization.html', + organization=organization, + current_user_member=current_user_member, + can_list_members=can_list_org_members(organization)) @app.route("/profile") @auth_required() def profile(): return render_template('profile.html') +@app.route("/events") +def events(): + events = Event.query.all() + return render_template('events.html', events=events) + +@app.route('/event/', methods=('GET', 'POST')) +def event(event_id): + event = Event.query.filter_by(id = event_id).first() + user_can_verify_event = can_verify_event(event) + + if user_can_verify_event and request.method == 'POST': + action = request.form['action'] + if action == 'verify': + event.verified = True + elif action == 'unverify': + event.verified = False + db.session.commit() + + return render_template('event.html', event=event, user_can_verify_event=user_can_verify_event) + +from forms.create_event import CreateEventForm + +@app.route("/events/create", methods=('GET', 'POST')) +def create_event(): + form = CreateEventForm() + if form.validate_on_submit(): + event = Event() + form.populate_obj(event) + event.admin_unit = get_admin_unit('Stadt Goslar') + eventDate = EventDate(event_id = event.id, start=form.start.data) + event.dates.append(eventDate) + db.session.commit() + return redirect(url_for('events')) + return render_template('create_event.html', form=form) + @app.route("/admin") @roles_required("admin") def admin(): @@ -73,7 +415,8 @@ def admin(): @app.route("/admin/admin_units") @roles_required("admin") def admin_admin_units(): - return render_template('admin/admin_units.html') + return render_template('admin/admin_units.html', + admin_units=AdminUnit.query.all()) if __name__ == '__main__': app.run() \ No newline at end of file diff --git a/forms/__init__.py b/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forms/create_event.py b/forms/create_event.py new file mode 100644 index 0000000..f3016b4 --- /dev/null +++ b/forms/create_event.py @@ -0,0 +1,13 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField +from wtforms.fields.html5 import DateTimeLocalField +from wtforms.validators import DataRequired, Optional + +class CreateEventForm(FlaskForm): + submit = SubmitField("Create event") + host = StringField('Host', validators=[DataRequired()]) + name = StringField('Name', validators=[DataRequired()]) + description = TextAreaField('Description', validators=[DataRequired()]) + start = DateTimeLocalField('Start', format='%Y-%m-%dT%H:%M', validators=[DataRequired()]) + location = StringField('Location', validators=[DataRequired()]) + external_link = StringField('Link URL', validators=[Optional()]) \ No newline at end of file diff --git a/manage.py b/manage.py index 6b77ceb..65630e3 100644 --- a/manage.py +++ b/manage.py @@ -1,4 +1,4 @@ -from flask_script import Manager +from flask_script import Manager, Command from flask_migrate import Migrate, MigrateCommand from app import app, db diff --git a/migrations/versions/a2ba246b23d0_.py b/migrations/versions/a2ba246b23d0_.py new file mode 100644 index 0000000..543cfae --- /dev/null +++ b/migrations/versions/a2ba246b23d0_.py @@ -0,0 +1,142 @@ +"""empty message + +Revision ID: a2ba246b23d0 +Revises: 36945eb387b4 +Create Date: 2020-06-14 19:39:43.809236 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a2ba246b23d0' +down_revision = '36945eb387b4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('adminunit', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('adminunitmemberrole', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=True), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('permissions', sa.UnicodeText(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('adminunitorgrole', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=True), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('permissions', sa.UnicodeText(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('organization', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('orgmemberrole', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=True), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('permissions', sa.UnicodeText(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('adminunitmember', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('admin_unit_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['admin_unit_id'], ['adminunit.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('adminunitorg', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('admin_unit_id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['admin_unit_id'], ['adminunit.id'], ), + sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('admin_unit_id', sa.Integer(), nullable=False), + sa.Column('host', sa.Unicode(length=255), nullable=False), + sa.Column('external_link', sa.String(length=255), nullable=True), + sa.Column('location', sa.Unicode(length=255), nullable=False), + sa.Column('name', sa.Unicode(length=255), nullable=False), + sa.Column('description', sa.UnicodeText(), nullable=False), + sa.Column('verified', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['admin_unit_id'], ['adminunit.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('orgmember', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('adminunitmemberroles_members', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('member_id', sa.Integer(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['member_id'], ['adminunitmember.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['adminunitmemberrole.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('adminunitorgroles_organizations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('admin_unit_org_id', sa.Integer(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['admin_unit_org_id'], ['adminunitorg.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['adminunitorgrole.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('eventdate', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False), + sa.Column('start', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('orgmemberroles_members', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('member_id', sa.Integer(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['member_id'], ['orgmember.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['orgmemberrole.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('orgmemberroles_members') + op.drop_table('eventdate') + op.drop_table('adminunitorgroles_organizations') + op.drop_table('adminunitmemberroles_members') + op.drop_table('orgmember') + op.drop_table('event') + op.drop_table('adminunitorg') + op.drop_table('adminunitmember') + op.drop_table('orgmemberrole') + op.drop_table('organization') + op.drop_table('adminunitorgrole') + op.drop_table('adminunitmemberrole') + op.drop_table('adminunit') + # ### end Alembic commands ### diff --git a/models.py b/models.py index 4137aff..a745373 100644 --- a/models.py +++ b/models.py @@ -1,8 +1,10 @@ from app import db from sqlalchemy.orm import relationship, backref -from sqlalchemy import Boolean, DateTime, Column, Integer, String, ForeignKey, UnicodeText +from sqlalchemy import Boolean, DateTime, Column, Integer, String, ForeignKey, Unicode, UnicodeText from flask_security import UserMixin, RoleMixin +### User + class RolesUsers(db.Model): __tablename__ = 'roles_users' id = Column(Integer(), primary_key=True) @@ -31,4 +33,117 @@ class User(db.Model, UserMixin): fs_uniquifier = Column(String(255)) confirmed_at = Column(DateTime()) roles = relationship('Role', secondary='roles_users', - backref=backref('users', lazy='dynamic')) \ No newline at end of file + backref=backref('users', lazy='dynamic')) + +### Organization + +class OrgMemberRolesMembers(db.Model): + __tablename__ = 'orgmemberroles_members' + id = Column(Integer(), primary_key=True) + member_id = Column('member_id', Integer(), ForeignKey('orgmember.id')) + role_id = Column('role_id', Integer(), ForeignKey('orgmemberrole.id')) + +class OrgMemberRole(db.Model, RoleMixin): + __tablename__ = 'orgmemberrole' + id = Column(Integer(), primary_key=True) + name = Column(String(80), unique=True) + description = Column(String(255)) + permissions = Column(UnicodeText()) + +class OrgMember(db.Model): + __tablename__ = 'orgmember' + id = Column(Integer(), primary_key=True) + organization_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user = db.relationship('User', backref=db.backref('orgmembers', lazy=True)) + + roles = relationship('OrgMemberRole', secondary='orgmemberroles_members', + backref=backref('members', lazy='dynamic')) + +class Organization(db.Model): + __tablename__ = 'organization' + id = Column(Integer(), primary_key=True) + name = Column(String(255), unique=True) + members = relationship('OrgMember', backref=backref('organization', lazy=True)) + +### Admin Unit + +class AdminUnitMemberRolesMembers(db.Model): + __tablename__ = 'adminunitmemberroles_members' + id = Column(Integer(), primary_key=True) + member_id = Column('member_id', Integer(), ForeignKey('adminunitmember.id')) + role_id = Column('role_id', Integer(), ForeignKey('adminunitmemberrole.id')) + +class AdminUnitMemberRole(db.Model, RoleMixin): + __tablename__ = 'adminunitmemberrole' + id = Column(Integer(), primary_key=True) + name = Column(String(80), unique=True) + description = Column(String(255)) + permissions = Column(UnicodeText()) + +class AdminUnitMember(db.Model): + __tablename__ = 'adminunitmember' + id = Column(Integer(), primary_key=True) + admin_unit_id = db.Column(db.Integer, db.ForeignKey('adminunit.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user = db.relationship('User', backref=db.backref('adminunitmembers', lazy=True)) + roles = relationship('AdminUnitMemberRole', secondary='adminunitmemberroles_members', + backref=backref('members', lazy='dynamic')) + +class AdminUnitOrgRoleOrganizations(db.Model): + __tablename__ = 'adminunitorgroles_organizations' + id = Column(Integer(), primary_key=True) + admin_unit_org_id = Column('admin_unit_org_id', Integer(), ForeignKey('adminunitorg.id')) + role_id = Column('role_id', Integer(), ForeignKey('adminunitorgrole.id')) + +class AdminUnitOrgRole(db.Model, RoleMixin): + __tablename__ = 'adminunitorgrole' + id = Column(Integer(), primary_key=True) + name = Column(String(80), unique=True) + description = Column(String(255)) + permissions = Column(UnicodeText()) + +class AdminUnitOrg(db.Model): + __tablename__ = 'adminunitorg' + id = Column(Integer(), primary_key=True) + admin_unit_id = db.Column(db.Integer, db.ForeignKey('adminunit.id'), nullable=False) + organization_id = db.Column(db.Integer, db.ForeignKey('organization.id'), nullable=False) + organization = db.relationship('Organization', backref=db.backref('adminunitorgs', lazy=True)) + roles = relationship('AdminUnitOrgRole', secondary='adminunitorgroles_organizations', + backref=backref('organizations', lazy='dynamic')) + +class AdminUnit(db.Model): + __tablename__ = 'adminunit' + id = Column(Integer(), primary_key=True) + name = Column(String(255), unique=True) + members = relationship('AdminUnitMember', backref=backref('adminunit', lazy=True)) + organizations = relationship('AdminUnitOrg', backref=backref('adminunit', lazy=True)) + +# Events +class Event(db.Model): + __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 = Column(Unicode(255), nullable=False) # org|adminunit|string + #co_hosts: -"- + #format: real|online + external_link = Column(String(255)) + #ticket_link = Column(String(255)) + location = Column(Unicode(255), nullable=False) # place|address + dates = relationship('EventDate', backref=backref('event', lazy=False)) + name = Column(Unicode(255), nullable=False) + description = Column(UnicodeText(), nullable=False) + #photo: image(1200x628) + #category: relationship, nullable=False + #keywords = Column(String(255)) oder liste? + #kid_friendly: bool + verified = Column(Boolean()) + +# (Multiple Events möglich, wiederholend oder frei, dann aber mit endzeit) +class EventDate(db.Model): + __tablename__ = 'eventdate' + 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 diff --git a/static/site.css b/static/site.css index b16f467..2ce20dd 100644 --- a/static/site.css +++ b/static/site.css @@ -1,7 +1,7 @@ h1 { - font-size: 1.8rem; - margin-bottom: 1.8rem; + font-size: 1.6rem; + margin: 1rem 0 2rem; } .navbar { @@ -40,4 +40,40 @@ footer { /* background-color: #009688; color: white; */ font-size: 1.5rem; +} + +tr.collapse.in { + display:table-row; +} + +tr.table-borderless td { + border:0; + padding:0; +} + +tr.table-line-through td { + text-decoration: line-through; +} + +.text-toogle[aria-expanded=false] .text-expanded { + display: none; +} +.text-toogle[aria-expanded=true] .text-collapsed { + display: none; +} + +.table td.fit, +.table th.fit { + white-space: nowrap; + width: 1%; +} + +.ui-autocomplete { + z-index: 1025 !important; +} + +@media (max-width: 320px) { + td, th { + word-break: break-all; + } } \ No newline at end of file diff --git a/templates/admin/admin.html b/templates/admin/admin.html index f5dedb6..aa9106a 100644 --- a/templates/admin/admin.html +++ b/templates/admin/admin.html @@ -11,7 +11,10 @@ {% endblock %} \ No newline at end of file diff --git a/templates/admin/admin_units.html b/templates/admin/admin_units.html index fe3df30..41436fe 100644 --- a/templates/admin/admin_units.html +++ b/templates/admin/admin_units.html @@ -11,5 +11,21 @@ +
+ + + + + + + + {% for admin_unit in admin_units %} + + + + {% endfor %} + +
{{ _('Name') }}
{{ admin_unit.name }}
+
{% endblock %} \ No newline at end of file diff --git a/templates/admin_unit.html b/templates/admin_unit.html new file mode 100644 index 0000000..422ddc8 --- /dev/null +++ b/templates/admin_unit.html @@ -0,0 +1,58 @@ +{% extends "layout.html" %} +{% block title %} +{{ admin_unit.name }} +{% endblock %} +{% block content %} + +

{{ admin_unit.name }}

+ + {% if current_user_member %} +
+ {{ _('You are a member of this admin unit.') }} + ({% for role in current_user_member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}) +
+ {% endif %} + + {% if can_list_admin_unit_members %} +

{{ _('Members') }}

+
+ + + + + + + + + {% for member in admin_unit.members %} + + + + + {% endfor %} + +
{{ _('Name') }}{{ _('Roles') }}
{{ member.user.email }}{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}
+
+ {% endif %} + +

{{ _('Organizations') }}

+
+ + + + + + + + + {% for admin_unit_org in admin_unit.organizations %} + + + + + {% endfor %} + +
{{ _('Name') }}{{ _('Roles') }}
{{ admin_unit_org.organization.name }}{% for role in admin_unit_org.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/admin_units.html b/templates/admin_units.html new file mode 100644 index 0000000..0432e28 --- /dev/null +++ b/templates/admin_units.html @@ -0,0 +1,26 @@ +{% extends "layout.html" %} +{% block title %} +{{ _('Admin Units') }} +{% endblock %} +{% block content %} + +

{{ _('Admin Units') }}

+ +
+ + + + + + + + {% for admin_unit in admin_units %} + + + + {% endfor %} + +
{{ _('Name') }}
{{ admin_unit.name }}
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/create_event.html b/templates/create_event.html new file mode 100644 index 0000000..2ebe448 --- /dev/null +++ b/templates/create_event.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field_with_errors, render_field %} + +{% block content %} + +

{{ _('Create event') }}

+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.host) }} + {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.description) }} + {{ render_field_with_errors(form.start) }} + {{ render_field_with_errors(form.location) }} + {{ render_field_with_errors(form.external_link) }} + {{ render_field(form.submit) }} +
+ +{% endblock %} diff --git a/templates/event.html b/templates/event.html new file mode 100644 index 0000000..9992012 --- /dev/null +++ b/templates/event.html @@ -0,0 +1,44 @@ +{% extends "layout.html" %} +{% block title %} +{{ event.name }} +{% endblock %} +{% block content %} + +

{{ event.name }}

+ + {% if user_can_verify_event %} +
+
+ {% if event.verified %} + +

+ {% else %} + +

+ {% endif %} +
+
+ {% endif %} + +
+
{{ event.dates[0].start | datetimeformat }}
+
{{ event.location }}
+ {% if event.verified %} +
{{ _('Verified') }}
+ {% endif %} +
+ +
{{ event.description }}
+ +{% if event.external_link %} + +{% endif %} + +
+
{{ event.host }}
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/events.html b/templates/events.html new file mode 100644 index 0000000..ee9942c --- /dev/null +++ b/templates/events.html @@ -0,0 +1,41 @@ +{% extends "layout.html" %} +{% block title %} +{{ _('Events') }} +{% endblock %} +{% block content %} + +

{{ _('Events') }}

+ + + +
+ + + + + + + + + + + {% for event in events %} + + + + + + + {% endfor %} + +
{{ _('Date') }}{{ _('Name') }}{{ _('Host') }}{{ _('Location') }}
{{ event.dates[0].start | datetimeformat }} + {{ event.name }} + {% if event.verified %} + + {% endif %} + {{ event.host }}{{ event.location }}
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 11a7b7d..b459ffa 100644 --- a/templates/home.html +++ b/templates/home.html @@ -4,6 +4,58 @@ Prototyp {% endblock %} {% block content %} - {{ _('Hi there!') }} +

{{ _('Hi there!') }}

+ + + + {% if admin_unit_members %} +

{{ _('Your Admin Units') }}

+
+ + + + + + + + + {% for member in admin_unit_members %} + + + + + {% endfor %} + +
{{ _('Name') }}{{ _('Roles') }}
{{ member.adminunit.name }}{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}
+
+ {% endif %} + + {% if organization_members %} +

{{ _('Your Organizations') }}

+
+ + + + + + + + + {% for member in organization_members %} + + + + + {% endfor %} + +
{{ _('Name') }}{{ _('Roles') }}
{{ member.organization.name }}{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}
+
+ {% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 34cfec7..42ebb6a 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -27,6 +27,10 @@ {% block header %} @@ -36,7 +40,7 @@ {% block body -%} {% block navbar %} -