From 1f6ff9f2ee60f6f324ee05eaea469eb7cbac973c Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sat, 26 Sep 2020 13:30:42 +0200 Subject: [PATCH] =?UTF-8?q?AU=20mit=20Mitglieder-Einladungen,=20die=20Even?= =?UTF-8?q?ts=20anlegen=20k=C3=B6nnen=20f=C3=BCr=20Vereine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 303 +++++++++++++++++-- forms/admin_unit_member.py | 31 ++ forms/widgets.py | 8 +- migrations/versions/8f4df40a36f3_.py | 38 +++ migrations/versions/a8c662c46047_.py | 32 ++ models.py | 14 + templates/_macros.html | 20 +- templates/admin_unit/invite_member.html | 24 ++ templates/admin_unit/read.html | 6 +- templates/admin_unit/update_member.html | 25 ++ templates/email/invitation_notice.html | 19 ++ templates/email/invitation_notice.txt | 3 + templates/email/layout.html | 382 ++++++++++++++++++++++++ templates/home.html | 4 +- templates/invitation/read.html | 27 ++ templates/manage/admin_units.html | 12 +- templates/manage/delete_invitation.html | 24 ++ templates/manage/delete_member.html | 24 ++ templates/manage/members.html | 49 +++ templates/organization/read.html | 4 +- templates/profile.html | 27 +- templates/security/login_user.html | 14 +- translations/de/LC_MESSAGES/messages.mo | Bin 8715 -> 10967 bytes translations/de/LC_MESSAGES/messages.po | 362 +++++++++++++++------- 24 files changed, 1306 insertions(+), 146 deletions(-) create mode 100644 forms/admin_unit_member.py create mode 100644 migrations/versions/8f4df40a36f3_.py create mode 100644 migrations/versions/a8c662c46047_.py create mode 100644 templates/admin_unit/invite_member.html create mode 100644 templates/admin_unit/update_member.html create mode 100644 templates/email/invitation_notice.html create mode 100644 templates/email/invitation_notice.txt create mode 100644 templates/email/layout.html create mode 100644 templates/invitation/read.html create mode 100644 templates/manage/delete_invitation.html create mode 100644 templates/manage/delete_member.html create mode 100644 templates/manage/members.html diff --git a/app.py b/app.py index 2f4f791..1a7c902 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,7 @@ from urllib.parse import quote_plus from dateutil.rrule import rrulestr, rruleset, rrule from dateutil.relativedelta import relativedelta from flask_qrcode import QRcode +from flask_mail import Mail, Message # Create app app = Flask(__name__) @@ -47,6 +48,16 @@ app.jinja_env.filters['quote_plus'] = lambda u: quote_plus(u) # cors cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) +# Mail +app.config['MAIL_SERVER'] = 'smtp.gmail.com' +app.config['MAIL_PORT'] = 587 +app.config['MAIL_USE_TLS'] = True +app.config['MAIL_USE_SSL'] = False +app.config['MAIL_USERNAME'] = 'oveda.app@gmail.com' +app.config['MAIL_PASSWORD'] = 'UPF7ujUi2zfa22E-' +app.config['MAIL_DEFAULT_SENDER'] = 'oveda.app@gmail.com' +mail = Mail(app) + # create db db = SQLAlchemy(app) @@ -58,7 +69,7 @@ app.json_encoder = DateTimeEncoder # Setup Flask-Security # Define models -from models import Analytics, EventRejectionReason, EventReviewStatus, EventPlace, EventOrganizer, EventCategory, Image, OrgOrAdminUnit, Actor, Place, Location, User, Role, AdminUnit, AdminUnitMember, AdminUnitMemberRole, OrgMember, OrgMemberRole, Organization, AdminUnitOrg, AdminUnitOrgRole, Event, EventDate +from models import AdminUnitMemberInvitation, Analytics, EventRejectionReason, EventReviewStatus, EventPlace, EventOrganizer, EventCategory, Image, 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 @@ -103,6 +114,8 @@ def print_dynamic_texts(): gettext('Event_Sports') gettext('Event_Other') gettext('Typical Age range') + gettext('Administrator') + gettext('Event expert') def handleSqlError(e): message = str(e.__dict__['orig']) @@ -120,6 +133,11 @@ def upsert_user(email, password="password"): result = user_datastore.create_user(email=email, password=hash_password(password)) return result +def add_roles_to_user(user_name, role_names): + user = upsert_user(user_name) + for role_name in role_names: + user_datastore.add_role_to_user(user, role_name) + def upsert_admin_unit(unit_name, short_name = None): admin_unit = AdminUnit.query.filter_by(name = unit_name).first() if admin_unit is None: @@ -143,16 +161,28 @@ def upsert_org_member_role(role_name, permissions): result.add_permissions(permissions) return result -def upsert_admin_unit_member_role(role_name, permissions): +def get_admin_unit_member_role(role_name): + return AdminUnitMemberRole.query.filter_by(name = role_name).first() + +def upsert_admin_unit_member_role(role_name, role_title, permissions): result = AdminUnitMemberRole.query.filter_by(name = role_name).first() if result is None: result = AdminUnitMemberRole(name = role_name) db.session.add(result) + result.title = role_title result.remove_permissions(result.get_permissions()) result.add_permissions(permissions) return result +def upsert_user_role(role_name, role_title, permissions): + role = user_datastore.find_or_create_role(role_name) + role.title = role_title + role.remove_permissions(role.get_permissions()) + role.add_permissions(permissions) + return role + + def upsert_admin_unit_org_role(role_name, permissions): result = AdminUnitOrgRole.query.filter_by(name = role_name).first() if result is None: @@ -179,6 +209,17 @@ def add_user_to_admin_unit(user, admin_unit): db.session.add(result) return result +def add_user_to_admin_unit_with_roles(user, admin_unit, role_names): + member = add_user_to_admin_unit(current_user, admin_unit) + add_roles_to_admin_unit_member(member, role_names) + + return member + +def add_roles_to_admin_unit_member(member, role_names): + for role_name in role_names: + role = get_admin_unit_member_role(role_name) + add_role_to_admin_unit_member(member, role) + 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: @@ -595,18 +636,21 @@ def has_current_user_permissions_for_admin_unit_and_any_org(admin_unit_id, org_m # Type permissions -def can_list_admin_unit_members(admin_unit): +def has_current_user_permission_for_admin_unit(admin_unit, permission): if not current_user.is_authenticated: return False - if has_current_user_permission('admin_unit.members:read'): + if has_current_user_permission(permission): return True - if has_current_user_member_permission_for_admin_unit(admin_unit.id, 'admin_unit.members:read'): + if has_current_user_member_permission_for_admin_unit(admin_unit.id, permission): return True return False +def can_list_admin_unit_members(admin_unit): + return has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:read') + def can_list_org_members(organization): if not current_user.is_authenticated: return False @@ -734,7 +778,27 @@ def get_pagination_urls(pagination, **kwargs): @app.before_first_request def create_initial_data(): - pass + admin_permissions = [ + "admin_unit:update", + "admin_unit.members:invite", + "admin_unit.members:read", + "admin_unit.members:update", + "admin_unit.members:delete", + "admin_unit.organizations.members:read"] + event_permissions = [ + "event:verify", + "event:create", + "event_suggestion:read"] + + upsert_admin_unit_member_role('admin', 'Administrator', admin_permissions) + upsert_admin_unit_member_role('event_verifier', 'Event expert', event_permissions) + + upsert_user_role('admin', 'Administrator', admin_permissions) + upsert_user_role('event_verifier', 'Event expert', event_permissions) + add_roles_to_user('grams.daniel@gmail.com', ['admin', 'event_verifier']) + + db.session.commit() + def flash_errors(form): for field, errors in form.errors.items(): @@ -744,12 +808,20 @@ def flash_errors(form): error ), 'danger') +def send_mail(recipient, subject, template, **context): + msg = Message(subject) + msg.recipients = [recipient] + msg.body = render_template("email/%s.txt" % template, **context) + msg.html = render_template("email/%s.html" % template, **context) + mail.send(msg) + # Views @app.route("/") def home(): if 'src' in request.args: track_analytics("home", '', request.args['src']) return redirect(url_for('home')) + return render_template('home.html') @app.route("/example") @@ -784,6 +856,39 @@ def admin_unit(admin_unit_id): can_list_admin_unit_members=can_list_admin_unit_members(admin_unit), can_update_admin_unit=has_current_user_permission('admin_unit:update')) +from forms.admin_unit_member import NegotiateAdminUnitMemberInvitationForm + +@app.route('/invitations/', methods=('GET', 'POST')) +@auth_required() +def admin_unit_member_invitation(id): + invitation = AdminUnitMemberInvitation.query.get_or_404(id) + + if invitation.email != current_user.email: + return permission_missing(url_for('profile')) + + form = NegotiateAdminUnitMemberInvitationForm() + + if form.validate_on_submit(): + try: + if form.accept.data: + message = gettext('Invitation successfully accepted') + roles = invitation.roles.split(',') + add_user_to_admin_unit_with_roles(current_user, invitation.adminunit, roles) + else: + message = gettext('Invitation successfully declined') + + db.session.delete(invitation) + db.session.commit() + flash(message, 'success') + return redirect(url_for('manage')) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), 'danger') + + return render_template('invitation/read.html', + form=form, + invitation=invitation) + def update_admin_unit_with_form(admin_unit, form): form.populate_obj(admin_unit) @@ -920,11 +1025,7 @@ def admin_unit_create(): upsert_org_or_admin_unit_for_admin_unit(admin_unit) # Aktuellen Nutzer als Admin hinzufügen - member = add_user_to_admin_unit(current_user, admin_unit) - admin_unit_admin_role = upsert_admin_unit_member_role('admin', ["admin_unit.members:read", "admin_unit.organizations.members:read"]) - admin_unit_event_verifier_role = upsert_admin_unit_member_role('event_verifier', ["event:verify", "event:create", "event_suggestion:read"]) - add_role_to_admin_unit_member(member, admin_unit_admin_role) - add_role_to_admin_unit_member(member, admin_unit_event_verifier_role) + add_user_to_admin_unit_with_roles(current_user, admin_unit, ['admin', 'event_verifier']) db.session.commit() # Organizer anlegen @@ -979,10 +1080,13 @@ def image(id): @app.route("/profile") @auth_required() def profile(): - 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 + admin_unit_members = AdminUnitMember.query.filter_by(user_id = current_user.id).all() + organization_members = None #OrgMember.query.filter_by(user_id = current_user.id).all() + invitations = AdminUnitMemberInvitation.query.filter(AdminUnitMemberInvitation.email == current_user.email).all() + return render_template('profile.html', admin_unit_members=admin_unit_members, + invitations=invitations, organization_members=organization_members) @app.route("/places") @@ -1201,6 +1305,7 @@ from forms.place import CreatePlaceForm, UpdatePlaceForm from forms.organization import CreateOrganizationForm, UpdateOrganizationForm from forms.organizer import CreateOrganizerForm, UpdateOrganizerForm, DeleteOrganizerForm from forms.admin_unit import CreateAdminUnitForm, UpdateAdminUnitForm +from forms.admin_unit_member import InviteAdminUnitMemberForm def update_event_with_form(event, form): form.populate_obj(event) @@ -1377,16 +1482,17 @@ def admin_admin_units(): admin_units=AdminUnit.query.all()) @app.route("/manage") +@auth_required() def manage(): admin_units = get_admin_units_for_manage() - - # if len(admin_units) == 1: - # return redirect(url_for('manage_admin_unit', id=admin_units[0].id)) + invitations = AdminUnitMemberInvitation.query.filter(AdminUnitMemberInvitation.email == current_user.email).all() return render_template('manage/admin_units.html', + invitations=invitations, admin_units=admin_units) @app.route('/manage/admin_unit/') +@auth_required() def manage_admin_unit(id): admin_unit = get_admin_unit_for_manage_or_404(id) @@ -1396,6 +1502,7 @@ def manage_admin_unit(id): admin_unit=admin_unit) @app.route('/manage/admin_unit//organizers') +@auth_required() def manage_admin_unit_organizers(id): admin_unit = get_admin_unit_for_manage_or_404(id) organizers = EventOrganizer.query.filter(EventOrganizer.admin_unit_id == admin_unit.id).order_by(func.lower(EventOrganizer.name)).paginate() @@ -1405,7 +1512,160 @@ def manage_admin_unit_organizers(id): organizers=organizers.items, pagination=get_pagination_urls(organizers, id=id)) +def permission_missing(redirect_location): + flash('You do not have permission for this action', 'danger') + return redirect(redirect_location) + +@app.route('/manage/admin_unit//members') +@auth_required() +def manage_admin_unit_members(id): + admin_unit = get_admin_unit_for_manage_or_404(id) + + if not has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:read'): + return permission_missing(url_for('manage_admin_unit', id=id)) + + members = AdminUnitMember.query.join(User).filter(AdminUnitMember.admin_unit_id == admin_unit.id).order_by(func.lower(User.email)).paginate() + invitations = AdminUnitMemberInvitation.query.filter(AdminUnitMember.admin_unit_id == admin_unit.id).order_by(func.lower(AdminUnitMemberInvitation.email)).all() + + return render_template('manage/members.html', + admin_unit=admin_unit, + can_invite_users=has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:invite'), + members=members.items, + invitations=invitations, + pagination=get_pagination_urls(members, id=id)) + +@app.route('/manage/admin_unit//members/invite', methods=('GET', 'POST')) +@auth_required() +def manage_admin_unit_member_invite(id): + admin_unit = get_admin_unit_for_manage_or_404(id) + + if not has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:invite'): + return permission_missing(url_for('manage_admin_unit', id=admin_unit.id)) + + form = InviteAdminUnitMemberForm() + form.roles.choices = [(c.name, gettext(c.title)) for c in AdminUnitMemberRole.query.order_by(AdminUnitMemberRole.id).all()] + + if form.validate_on_submit(): + invitation = AdminUnitMemberInvitation() + invitation.admin_unit_id = admin_unit.id + form.populate_obj(invitation) + invitation.roles = ','.join(form.roles.data) + + try: + db.session.add(invitation) + db.session.commit() + + send_mail(invitation.email, + gettext('You have received an invitation'), + 'invitation_notice', + invitation=invitation) + + flash(gettext('Invitation successfully sent'), 'success') + return redirect(url_for('manage_admin_unit_members', id=admin_unit.id)) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), 'danger') + return render_template('admin_unit/invite_member.html', + admin_unit=admin_unit, + form=form) + +from forms.admin_unit_member import DeleteAdminUnitMemberForm, UpdateAdminUnitMemberForm + +@app.route('/manage/member//update', methods=('GET', 'POST')) +@auth_required() +def manage_admin_unit_member_update(id): + member = AdminUnitMember.query.get_or_404(id) + admin_unit = member.adminunit + + if not has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:update'): + return permission_missing(url_for('manage_admin_unit', id=admin_unit.id)) + + form = UpdateAdminUnitMemberForm() + form.roles.choices = [(c.name, gettext(c.title)) for c in AdminUnitMemberRole.query.order_by(AdminUnitMemberRole.id).all()] + + if form.validate_on_submit(): + member.roles.clear() + add_roles_to_admin_unit_member(member, form.roles.data) + + try: + db.session.commit() + flash(gettext('Member successfully updated'), 'success') + return redirect(url_for('manage_admin_unit_members', id=admin_unit.id)) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), 'danger') + else: + form.roles.data = [c.name for c in member.roles] + + return render_template('admin_unit/update_member.html', + admin_unit=admin_unit, + member=member, + form=form) + +@app.route('/manage/member//delete', methods=('GET', 'POST')) +@auth_required() +def manage_admin_unit_member_delete(id): + member = AdminUnitMember.query.get_or_404(id) + admin_unit = member.adminunit + + if not has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:delete'): + return permission_missing(url_for('manage_admin_unit', id=admin_unit.id)) + + form = DeleteAdminUnitMemberForm() + + if form.validate_on_submit(): + if form.email.data != member.user.email: + flash(gettext('Entered email does not match member email'), 'danger') + else: + try: + db.session.delete(member) + db.session.commit() + flash(gettext('Member successfully deleted'), 'success') + return redirect(url_for('manage_admin_unit_members', id=admin_unit.id)) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), 'danger') + else: + flash_errors(form) + + return render_template('manage/delete_member.html', + form=form, + member=member) + +from forms.admin_unit_member import DeleteAdminUnitInvitationForm + +@app.route('/manage/invitation//delete', methods=('GET', 'POST')) +@auth_required() +def manage_admin_unit_invitation_delete(id): + invitation = AdminUnitMemberInvitation.query.get_or_404(id) + admin_unit = invitation.adminunit + + if not has_current_user_permission_for_admin_unit(admin_unit, 'admin_unit.members:invite'): + return permission_missing(url_for('manage_admin_unit', id=id)) + + form = DeleteAdminUnitInvitationForm() + + if form.validate_on_submit(): + if form.email.data != invitation.email: + flash(gettext('Entered email does not match invitation email'), 'danger') + else: + try: + db.session.delete(invitation) + db.session.commit() + flash(gettext('Invitation successfully deleted'), 'success') + return redirect(url_for('manage_admin_unit_members', id=admin_unit.id)) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), 'danger') + else: + flash_errors(form) + + return render_template('manage/delete_invitation.html', + form=form, + invitation=invitation) + @app.route('/manage/admin_unit//event_places') +@auth_required() def manage_admin_unit_event_places(id): admin_unit = get_admin_unit_for_manage_or_404(id) organizer = EventOrganizer.query.filter(EventOrganizer.admin_unit_id == admin_unit.id).order_by(func.lower(EventOrganizer.name)).first() @@ -1419,6 +1679,7 @@ def manage_admin_unit_event_places(id): from forms.event_place import FindEventPlaceForm @app.route('/manage/event_places') +@auth_required() def manage_organizer_event_places(): organizer = EventOrganizer.query.get_or_404(request.args.get('organizer_id')) admin_unit = get_admin_unit_for_manage_or_404(organizer.admin_unit_id) @@ -1436,6 +1697,7 @@ def manage_organizer_event_places(): pagination=get_pagination_urls(places)) @app.route('/manage/admin_unit//events') +@auth_required() def manage_admin_unit_events(id): admin_unit = get_admin_unit_for_manage_or_404(id) organizer = EventOrganizer.query.filter(EventOrganizer.admin_unit_id == admin_unit.id).order_by(func.lower(EventOrganizer.name)).first() @@ -1449,6 +1711,7 @@ def manage_admin_unit_events(id): from forms.event import FindEventForm @app.route('/manage/events') +@auth_required() def manage_organizer_events(): organizer = EventOrganizer.query.get_or_404(request.args.get('organizer_id')) admin_unit = get_admin_unit_for_manage_or_404(organizer.admin_unit_id) @@ -1474,6 +1737,7 @@ def manage_organizer_events(): pagination=get_pagination_urls(events)) @app.route('/manage/admin_unit//reviews') +@auth_required() def manage_admin_unit_event_reviews(id): admin_unit = get_admin_unit_for_manage_or_404(id) @@ -1490,6 +1754,7 @@ def manage_admin_unit_event_reviews(id): pagination = get_pagination_urls(events_paginate, id=id)) @app.route('/manage/admin_unit//widgets') +@auth_required() def manage_admin_unit_widgets(id): admin_unit = get_admin_unit_for_manage_or_404(id) return render_template('manage/widgets.html', admin_unit=admin_unit) @@ -1512,6 +1777,7 @@ def update_organizer_with_form(organizer, form): organizer.logo = upsert_image_with_data(organizer.logo, fs.read(), fs.content_type) @app.route('/manage/admin_unit//organizers/create', methods=('GET', 'POST')) +@auth_required() def manage_admin_unit_organizer_create(id): admin_unit = get_admin_unit_for_manage_or_404(id) @@ -1527,7 +1793,6 @@ def manage_admin_unit_organizer_create(id): db.session.add(organizer) db.session.commit() flash(gettext('Organizer successfully created'), 'success') - #return redirect(url_for('organizer', id=organizer.id)) return redirect(url_for('manage_admin_unit_organizers', id=organizer.admin_unit_id)) except SQLAlchemyError as e: db.session.rollback() @@ -1535,6 +1800,7 @@ def manage_admin_unit_organizer_create(id): return render_template('organizer/create.html', form=form) @app.route('/organizer//update', methods=('GET', 'POST')) +@auth_required() def organizer_update(id): organizer = EventOrganizer.query.get_or_404(id) @@ -1560,6 +1826,7 @@ def organizer_update(id): organizer=organizer) @app.route('/organizer//delete', methods=('GET', 'POST')) +@auth_required() def organizer_delete(id): organizer = EventOrganizer.query.get_or_404(id) @@ -1597,6 +1864,7 @@ def update_event_place_with_form(place, form): from forms.event_place import UpdateEventPlaceForm, CreateEventPlaceForm @app.route('/event_place//update', methods=('GET', 'POST')) +@auth_required() def event_place_update(id): place = EventPlace.query.get_or_404(id) @@ -1621,6 +1889,7 @@ def event_place_update(id): place=place) @app.route('/manage/organizer//places/create', methods=('GET', 'POST')) +@auth_required() def manage_organizer_places_create(id): organizer = EventOrganizer.query.get_or_404(id) diff --git a/forms/admin_unit_member.py b/forms/admin_unit_member.py new file mode 100644 index 0000000..ebee78c --- /dev/null +++ b/forms/admin_unit_member.py @@ -0,0 +1,31 @@ +from flask_babelex import lazy_gettext +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from wtforms import StringField, SubmitField, DecimalField, TextAreaField, FormField, SelectField +from wtforms.fields.html5 import EmailField, TelField +from wtforms.validators import DataRequired, Optional, Regexp +import decimal +from models import Location +from .widgets import MultiCheckboxField + +class InviteAdminUnitMemberForm(FlaskForm): + email = EmailField(lazy_gettext('Email'), validators=[DataRequired()]) + roles = MultiCheckboxField(lazy_gettext('Roles')) + submit = SubmitField(lazy_gettext("Invite")) + +class NegotiateAdminUnitMemberInvitationForm(FlaskForm): + accept = SubmitField(lazy_gettext("Accept")) + decline = SubmitField(lazy_gettext("Decline")) + +class DeleteAdminUnitInvitationForm(FlaskForm): + submit = SubmitField(lazy_gettext("Delete invitation")) + email = EmailField(lazy_gettext('Email'), validators=[DataRequired()]) + +class DeleteAdminUnitMemberForm(FlaskForm): + submit = SubmitField(lazy_gettext("Delete member")) + email = EmailField(lazy_gettext('Email'), validators=[DataRequired()]) + +class UpdateAdminUnitMemberForm(FlaskForm): + roles = MultiCheckboxField(lazy_gettext('Roles')) + submit = SubmitField(lazy_gettext("Update member")) + diff --git a/forms/widgets.py b/forms/widgets.py index 9f6eedb..027d349 100644 --- a/forms/widgets.py +++ b/forms/widgets.py @@ -1,9 +1,13 @@ -from wtforms import DateTimeField -from wtforms.widgets import html_params, HTMLString +from wtforms import DateTimeField, SelectMultipleField +from wtforms.widgets import html_params, HTMLString, ListWidget, CheckboxInput import pytz from datetime import datetime from flask_babelex import to_user_timezone +class MultiCheckboxField(SelectMultipleField): + widget = ListWidget(prefix_label=False) + option_widget = CheckboxInput() + def create_option_string(count, value): result = "" for i in range(count): diff --git a/migrations/versions/8f4df40a36f3_.py b/migrations/versions/8f4df40a36f3_.py new file mode 100644 index 0000000..cfdaf54 --- /dev/null +++ b/migrations/versions/8f4df40a36f3_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: 8f4df40a36f3 +Revises: f71c86333bfb +Create Date: 2020-09-24 18:53:02.861732 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +import db + + +# revision identifiers, used by Alembic. +revision = '8f4df40a36f3' +down_revision = 'f71c86333bfb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('adminunitmemberinvitation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('admin_unit_id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('roles', sa.UnicodeText(), nullable=True), + sa.ForeignKeyConstraint(['admin_unit_id'], ['adminunit.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email', 'admin_unit_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('adminunitmemberinvitation') + # ### end Alembic commands ### diff --git a/migrations/versions/a8c662c46047_.py b/migrations/versions/a8c662c46047_.py new file mode 100644 index 0000000..b529cff --- /dev/null +++ b/migrations/versions/a8c662c46047_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: a8c662c46047 +Revises: 8f4df40a36f3 +Create Date: 2020-09-25 11:26:03.139800 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +import db + + +# revision identifiers, used by Alembic. +revision = 'a8c662c46047' +down_revision = '8f4df40a36f3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('adminunitmemberrole', sa.Column('title', sa.Unicode(length=255), nullable=True)) + op.add_column('role', sa.Column('title', sa.Unicode(length=255), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('role', 'title') + op.drop_column('adminunitmemberrole', 'title') + # ### end Alembic commands ### diff --git a/models.py b/models.py index eb36c3e..b93f46e 100644 --- a/models.py +++ b/models.py @@ -45,6 +45,7 @@ class Role(db.Model, RoleMixin): __tablename__ = 'role' id = Column(Integer(), primary_key=True) name = Column(String(80), unique=True) + title = Column(Unicode(255)) description = Column(String(255)) permissions = Column(UnicodeText()) @@ -122,6 +123,7 @@ class AdminUnitMemberRole(db.Model, RoleMixin): __tablename__ = 'adminunitmemberrole' id = Column(Integer(), primary_key=True) name = Column(String(80), unique=True) + title = Column(Unicode(255)) description = Column(String(255)) permissions = Column(UnicodeText()) @@ -132,8 +134,19 @@ class AdminUnitMember(db.Model): 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', + order_by="AdminUnitMemberRole.id", backref=backref('members', lazy='dynamic')) +class AdminUnitMemberInvitation(db.Model): + __tablename__ = 'adminunitmemberinvitation' + __table_args__ = ( + UniqueConstraint('email', 'admin_unit_id'), + ) + id = Column(Integer(), primary_key=True) + admin_unit_id = db.Column(db.Integer, db.ForeignKey('adminunit.id'), nullable=False) + email = Column(String(255)) + roles = Column(UnicodeText()) + class AdminUnitOrgRoleOrganizations(db.Model): __tablename__ = 'adminunitorgroles_organizations' id = Column(Integer(), primary_key=True) @@ -162,6 +175,7 @@ class AdminUnit(db.Model, TrackableMixin): name = Column(Unicode(255), unique=True) short_name = Column(Unicode(100), unique=True) members = relationship('AdminUnitMember', backref=backref('adminunit', lazy=True)) + invitations = relationship('AdminUnitMemberInvitation', backref=backref('adminunit', lazy=True)) organizations = relationship('AdminUnitOrg', backref=backref('adminunit', lazy=True)) event_organizers = relationship('EventOrganizer', backref=backref('adminunit', lazy=True)) event_places = relationship('EventPlace', backref=backref('adminunit', lazy=True)) diff --git a/templates/_macros.html b/templates/_macros.html index 7e985aa..2da465c 100644 --- a/templates/_macros.html +++ b/templates/_macros.html @@ -9,7 +9,18 @@ {% set field_class = field_class + ' is-invalid' %} {% endif %} - {{ field(class=field_class, **kwargs)|safe }} + {% if 'ri' in kwargs and kwargs['ri'] == 'multicheckbox' %} +
+ {% for choice in field %} +
+ {{ choice(class="form-check-input") }} + {{ choice.label(class="form-check-label") }} +
+ {% endfor %} +
+ {% else %} + {{ field(class=field_class, **kwargs)|safe }} + {% endif %} {% if 'ri' in kwargs %} {% if kwargs['ri'] == 'rrule' %} @@ -475,6 +486,7 @@ {{ render_tab('events', _('Events'), url_for('manage_admin_unit_events', id=admin_unit.id), active_id) }} {{ render_tab('organizers', _('Organizers'), url_for('manage_admin_unit_organizers', id=admin_unit.id), active_id) }} {{ render_tab('places', _('Places'), url_for('manage_admin_unit_event_places', id=admin_unit.id), active_id) }} + {{ render_tab('members', _('Members'), url_for('manage_admin_unit_members', id=admin_unit.id), active_id) }} {{ render_tab('widgets', _('Widgets'), url_for('manage_admin_unit_widgets', id=admin_unit.id), active_id) }} {% endmacro %} @@ -527,4 +539,10 @@ $( function() { {% endif %} +{% endmacro %} + +{% macro render_roles(roles) %} +{% if roles %} + {% for role in roles %}{{ _(role.title) }}{%if not loop.last %}, {% endif %}{% endfor %} +{% endif %} {% endmacro %} \ No newline at end of file diff --git a/templates/admin_unit/invite_member.html b/templates/admin_unit/invite_member.html new file mode 100644 index 0000000..fb4ed1b --- /dev/null +++ b/templates/admin_unit/invite_member.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_manage_menu, render_field_with_errors, render_field %} +{% block content %} + +

{{ admin_unit.name }}

+ +{{ render_manage_menu(admin_unit, 'members') }} + +

{{ _('Invite user') }}

+
+ {{ form.hidden_tag() }} + +
+
+ {{ render_field_with_errors(form.email) }} + {{ render_field_with_errors(form.roles, ri="multicheckbox") }} + +
+
+ + {{ render_field(form.submit) }} +
+ +{% endblock %} diff --git a/templates/admin_unit/read.html b/templates/admin_unit/read.html index 006a5af..c3368dd 100644 --- a/templates/admin_unit/read.html +++ b/templates/admin_unit/read.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_string_prop, render_logo, render_phone_prop, render_fax_prop, render_email_prop, render_events, render_location_prop, render_link_prop, render_image %} +{% from "_macros.html" import render_roles, render_string_prop, render_logo, render_phone_prop, render_fax_prop, render_email_prop, render_events, render_location_prop, render_link_prop, render_image %} {% block title %} {{ admin_unit.name }} {% endblock %} @@ -71,7 +71,7 @@ {% for member in admin_unit.members %} {{ member.user.email }} - {% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %} + {{ render_roles(member.roles)}} {% endfor %} @@ -93,7 +93,7 @@ {% for admin_unit_org in admin_unit.organizations %} {{ admin_unit_org.organization.name }} - {% for role in admin_unit_org.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %} + {{ render_roles(admin_unit_org.roles)}} {% endfor %} diff --git a/templates/admin_unit/update_member.html b/templates/admin_unit/update_member.html new file mode 100644 index 0000000..ef45010 --- /dev/null +++ b/templates/admin_unit/update_member.html @@ -0,0 +1,25 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_manage_menu, render_field_with_errors, render_field %} +{% block content %} + +

{{ admin_unit.name }}

+ +{{ render_manage_menu(admin_unit, 'members') }} + +

{{ _('Update member') }}

+
+ {{ form.hidden_tag() }} + +
+
+ {{ member.user.email }} +
+
+ {{ render_field_with_errors(form.roles, ri="multicheckbox") }} +
+
+ + {{ render_field(form.submit) }} +
+ +{% endblock %} diff --git a/templates/email/invitation_notice.html b/templates/email/invitation_notice.html new file mode 100644 index 0000000..9e6e36b --- /dev/null +++ b/templates/email/invitation_notice.html @@ -0,0 +1,19 @@ +{% extends "email/layout.html" %} +{% block content %} +

{{ _('You have been invited to join %(admin_unit_name)s.', admin_unit_name=invitation.adminunit.name) }}

+ + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/email/invitation_notice.txt b/templates/email/invitation_notice.txt new file mode 100644 index 0000000..a029149 --- /dev/null +++ b/templates/email/invitation_notice.txt @@ -0,0 +1,3 @@ +{{ _('You have been invited to join %(admin_unit_name)s.', admin_unit_name=invitation.adminunit.name) }} +{{ _('Click the link below to view the invitation') }} +{{ url_for('admin_unit_member_invitation', id=invitation.id, _external=True) }} diff --git a/templates/email/layout.html b/templates/email/layout.html new file mode 100644 index 0000000..2884770 --- /dev/null +++ b/templates/email/layout.html @@ -0,0 +1,382 @@ + + + + + + Email + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 723a117..7a7842c 100644 --- a/templates/home.html +++ b/templates/home.html @@ -22,7 +22,7 @@ oveda - Offene Veranstaltungsdatenbank
@@ -138,7 +138,7 @@ oveda - Offene Veranstaltungsdatenbank
diff --git a/templates/invitation/read.html b/templates/invitation/read.html new file mode 100644 index 0000000..104f662 --- /dev/null +++ b/templates/invitation/read.html @@ -0,0 +1,27 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field %} +{% block title %} +{{ _('Invitation') }} +{% endblock %} +{% block content %} + +

{{ _('Invitation') }}

+ +

{{ _('Would you like to accept the invitation from %(name)s?', name=invitation.adminunit.name) }}

+ + + {{ form.hidden_tag() }} + +
+
+
+ {{ render_field(form.accept, class='btn btn-success') }} +
+
+ {{ render_field(form.decline, class='btn btn-secondary') }} +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/manage/admin_units.html b/templates/manage/admin_units.html index 2d7b58d..5bd8831 100644 --- a/templates/manage/admin_units.html +++ b/templates/manage/admin_units.html @@ -4,11 +4,21 @@ {% endblock %} {% block content %} +{% if invitations %} +

{{ _('Invitations') }}

+
+ {% for invitation in invitations %} + {{ invitation.adminunit.name }} + {% endfor %} +
+{% endif %} + +

{{ _('Admin Units') }}

-
+
{% for admin_unit in admin_units %} {{ admin_unit.name }} {% endfor %} diff --git a/templates/manage/delete_invitation.html b/templates/manage/delete_invitation.html new file mode 100644 index 0000000..9cc9524 --- /dev/null +++ b/templates/manage/delete_invitation.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field_with_errors, render_field %} + +{% block content %} + +

{{ _('Delete invitation') }} "{{ invitation.email }}"

+ +
+ {{ form.hidden_tag() }} + +
+
+ {{ _('Invitation') }} +
+
+ {{ render_field_with_errors(form.email) }} +
+
+ + {{ render_field(form.submit) }} + +
+ +{% endblock %} diff --git a/templates/manage/delete_member.html b/templates/manage/delete_member.html new file mode 100644 index 0000000..2248ddf --- /dev/null +++ b/templates/manage/delete_member.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field_with_errors, render_field %} + +{% block content %} + +

{{ _('Delete member') }} "{{ member.user.email }}"

+ +
+ {{ form.hidden_tag() }} + +
+
+ {{ _('Member') }} +
+
+ {{ render_field_with_errors(form.email) }} +
+
+ + {{ render_field(form.submit) }} + +
+ +{% endblock %} diff --git a/templates/manage/members.html b/templates/manage/members.html new file mode 100644 index 0000000..cb73cc1 --- /dev/null +++ b/templates/manage/members.html @@ -0,0 +1,49 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_roles, render_pagination, render_event_organizer, render_manage_menu %} +{% block title %} +{{ _('Members') }} +{% endblock %} +{% block content %} + +

{{ admin_unit.name }}

+ +{{ render_manage_menu(admin_unit, 'members') }} + +

{{ _('Invitations') }}

+
+ {% if can_invite_users %} + {{ _('Invite user') }} + {% endif %} +
+
    +{% for invitation in invitations %} +
  • + +
  • +{% endfor %} +
+ +

{{ _('Members') }}

+ + +
{{ render_pagination(pagination) }}
+ +{% endblock %} \ No newline at end of file diff --git a/templates/organization/read.html b/templates/organization/read.html index 3d2e8bf..0069cd0 100644 --- a/templates/organization/read.html +++ b/templates/organization/read.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_string_prop, render_logo, render_phone_prop, render_fax_prop, render_email_prop, render_events, render_location_prop, render_link_prop, render_image %} +{% from "_macros.html" import render_rolesm render_string_prop, render_logo, render_phone_prop, render_fax_prop, render_email_prop, render_events, render_location_prop, render_link_prop, render_image %} {% block title %} {{ organization.name }} {% endblock %} @@ -69,7 +69,7 @@ {% for member in organization.members %} {{ member.user.email }} - {% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %} + {{ render_roles(member.roles)}} {% endfor %} diff --git a/templates/profile.html b/templates/profile.html index bc68815..052088c 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -1,4 +1,5 @@ {% extends "layout.html" %} +{% from "_macros.html" import render_roles %} {% block title %} {{ _('Profile') }} {% endblock %} @@ -6,6 +7,26 @@

{{ current_user.email }}

+{% if invitations %} +

{{ _('Invitations') }}

+
+ + + + + + + + {% for invitation in invitations %} + + + + {% endfor %} + +
{{ _('Name') }}
{{ invitation.adminunit.name }}
+
+{% endif %} + {% if admin_unit_members %}

{{ _('Admin Units') }}

@@ -19,8 +40,8 @@ {% for member in admin_unit_members %} - {{ member.adminunit.name }} - {% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %} + {{ member.adminunit.name }} + {{ render_roles(member.roles)}} {% endfor %} @@ -42,7 +63,7 @@ {% for member in organization_members %} {{ member.organization.name }} - {% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %} + {{ render_roles(member.roles)}} {% endfor %} diff --git a/templates/security/login_user.html b/templates/security/login_user.html index 4818e4e..541996f 100644 --- a/templates/security/login_user.html +++ b/templates/security/login_user.html @@ -18,8 +18,16 @@ {{ render_field(login_user_form.submit) }} -
- -{{ render_google_sign_in_button() }} +
+
+

{{ _('You do not have an account yet? Not a problem!') }}

+ +
+ {{ render_google_sign_in_button() }} +
+
+
{% endblock %} diff --git a/translations/de/LC_MESSAGES/messages.mo b/translations/de/LC_MESSAGES/messages.mo index d2d1fca215a9f4a48a6c7be9a74db858d0f45e01..aa2b6d6edf74131e2d8f6a52fecb2825e1171d22 100644 GIT binary patch literal 10967 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 HS>gWz>d!v3 delta 3007 zcmYM!dra0<9LMnkqCj#{E^&v4TXC5Q84zkq9Hs>3WjdLcDdYn^DGvw_l~z8@oTi3t z9?g}NF3Zd=bKPSFzy7(K zs)Ikqg>5tZy~lq#{}YF+_W$3)dyGk>x(GwC1LLt1@5K$MM|R;z+=Kkg0WMkCgPC~A zwco{9>fv;bFeYe{D1_1A!EnsS(KrU9@KMx+bvO!_xb_vO=T_qgeA%^cMD^Q_{LEf1 zk$4c*|4SqWa~y{=zd1u;7!Bu9kzK(1@ER6jEK#K5c+^VgBDc%})IiPde!H{FxffHo ze;m{Bl54+>%1A;q8E1YojzSL3#3bxMO}NRq2ek!XyZimF{)ej%j~Tk3ftqL>D)lo^ z6Sbl;v>BC&ZK%L^Vvu0WJ_<_N5!A{~qT0_pe@1@hIv3_N|GM^(q)Yvioq4DXmbm&P zSFb}ozYsO{}zD(3%o8qi>&O)7ydCqoJz+I?`wxSla3-#7~hzh(1 zS+wazrZRn~z=KyPDAEB`$B?+8i6c=1#-dJf8tM?1VFA{n0uJDK45B9Z%y|kGz(wbE z)Ph23lo3eaK@&4nFo~$spN>lLL#PNVk)L^t%UGO`)wmHg;aSv*E+cC&H&BNyG-0Sd z8Z}NXa?6yU`d1IOv;T7`C}oYPiI<^L+J<_ap2a-0sI57Q58`DE#W>Q^#iub56~Jyj zi~3R?LS^g-YRgWe0=tGhYHnei-v20;eLrSlF-}FLz5{h=g3b+2i`uHUQJ?Jhk)Qd7 zOEO+`4xlm=MWa%lj#_9AGPfzlpdy@1K`E+7?Wxzu4 zi5jn*1EVulhYEB)Dx;0451kKl@zoUeUn$*Bg9hwJJ@6~)fxl1@-$rF5lsqaUF{nV3 zT|F0de+;UBC2A`sqx#K31+*A7ehVsdovGwMgTfXXQQ^S#NBU0eJNL> z0)7#-r)yD}*x~MffZE!RP=Opp1#%n*&(=u_I*sR04_-yR9s{V3VdQCWMb0=>dm3s0 z59+xh)CA?uDpbEZRDiQ^@Qk6plwQo&`@f!oUW-pq1AT$&coNmI4|Qm6ArF{T_DT0A zVF^x0WoR{O!gZ+3Y(mbXc@25g>~;5hQO}>kaOOAt6m)7Yqaytqbr|oWQXb0(Tlce2 z^)l2-r=a@JMD?GC>i0Bq9L!5tiaSslJdM@(JuW7@?eWFkAqnq>bRSBb5NgAc^KC`)19l_-w~`C98``~|22YE(rP15&X>IZZ zp7Jq5dvk-|x2hrFYxUdIlro!;+7wY*?kO!BU;1$On$(IA+nd&7ZF-Ttn!eTsGCcNJ z#tQpWW~n`!xy8Pf6>aO%qwL|VBs(E5p}QyR&k)<4onhDHJZ=YaD!K!?--dM0%3l;} zM-SV3*FugSN{=WP#|D*r04ui5K2^J=Q6PMKq?3N!4E!s5K@PEVt+BjB0t e3p6kHd6#;Y`n(;4J!3H3j-haS3#-HEW&Q)I22bMv diff --git a/translations/de/LC_MESSAGES/messages.po b/translations/de/LC_MESSAGES/messages.po index c9aff75..279098d 100644 --- a/translations/de/LC_MESSAGES/messages.po +++ b/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-09-16 15:18+0200\n" +"POT-Creation-Date: 2020-09-26 13:22+0200\n" "PO-Revision-Date: 2020-06-07 18:51+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -18,152 +18,196 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: app.py:73 +#: app.py:88 msgid "Event_" msgstr "Event_" -#: app.py:78 +#: app.py:93 msgid "." msgstr "." -#: app.py:83 +#: app.py:98 msgid "Event_Art" msgstr "Kunst" -#: app.py:84 +#: app.py:99 msgid "Event_Book" msgstr "Literatur" -#: app.py:85 +#: app.py:100 msgid "Event_Movie" msgstr "Film" -#: app.py:86 +#: app.py:101 msgid "Event_Family" msgstr "Familie" -#: app.py:87 +#: app.py:102 msgid "Event_Festival" msgstr "Festival" -#: app.py:88 +#: app.py:103 msgid "Event_Religious" msgstr "Religion" -#: app.py:89 +#: app.py:104 msgid "Event_Shopping" msgstr "Shopping" -#: app.py:90 +#: app.py:105 msgid "Event_Comedy" msgstr "Comedy" -#: app.py:91 +#: app.py:106 msgid "Event_Music" msgstr "Musik" -#: app.py:92 +#: app.py:107 msgid "Event_Dance" msgstr "Tanz" -#: app.py:93 +#: app.py:108 msgid "Event_Nightlife" msgstr "Party" -#: app.py:94 +#: app.py:109 msgid "Event_Theater" msgstr "Theater" -#: app.py:95 +#: app.py:110 msgid "Event_Dining" msgstr "Essen" -#: app.py:96 +#: app.py:111 msgid "Event_Conference" msgstr "Konferenz" -#: app.py:97 +#: app.py:112 msgid "Event_Meetup" msgstr "Networking" -#: app.py:98 +#: app.py:113 msgid "Event_Fitness" msgstr "Fitness" -#: app.py:99 +#: app.py:114 msgid "Event_Sports" msgstr "Sport" -#: app.py:100 +#: app.py:115 msgid "Event_Other" msgstr "Sonstiges" -#: app.py:101 +#: app.py:116 msgid "Typical Age range" msgstr "Typische Altersspanne" -#: app.py:727 +#: app.py:117 +msgid "Administrator" +msgstr "Administrator:in" + +#: app.py:118 +msgid "Event expert" +msgstr "Veranstaltungsexpert:in" + +#: app.py:806 #, python-format msgid "Error in the %s field - %s" msgstr "Fehler im Feld %s: %s" -#: app.py:849 +#: app.py:874 +msgid "Invitation successfully accepted" +msgstr "Einladung erfolgreich akzeptiert" + +#: app.py:878 +msgid "Invitation successfully declined" +msgstr "Einladung erfolgreich abgelehnt" + +#: app.py:972 msgid "Organization successfully created" msgstr "Organisation erfolgreich erstellt" -#: app.py:870 +#: app.py:993 msgid "Organization successfully updated" msgstr "Organisation erfolgreich aktualisiert" -#: app.py:927 +#: app.py:1046 msgid "Admin unit successfully created" msgstr "Verwaltungseinheit erfolgreich erstellt" -#: app.py:946 +#: app.py:1065 msgid "AdminUnit successfully updated" msgstr "Verwaltungseinheit erfolgreich aktualisiert" -#: app.py:1006 app.py:1546 +#: app.py:1128 app.py:1881 msgid "Place successfully updated" msgstr "Ort erfolgreich aktualisiert" -#: app.py:1030 app.py:1575 +#: app.py:1152 app.py:1911 msgid "Place successfully created" msgstr "Ort erfolgreich erstellt" -#: app.py:1081 app.py:1251 +#: app.py:1203 app.py:1418 msgid "Event successfully updated" msgstr "Veranstaltung erfolgreich aktualisiert" -#: app.py:1206 +#: app.py:1373 msgid "Event successfully created" msgstr "Veranstaltung erfolgreich erstellt" -#: app.py:1209 +#: app.py:1376 msgid "Thank you so much! The event is being verified." msgstr "Vielen Dank! Die Veranstaltung wird geprüft." -#: app.py:1274 +#: app.py:1441 msgid "Entered name does not match event name" msgstr "Der eingegebene Name entspricht nicht dem Namen der Veranstaltung" -#: app.py:1279 +#: app.py:1446 msgid "Event successfully deleted" msgstr "Veranstaltung erfolgreich gelöscht" -#: app.py:1462 +#: app.py:1559 +msgid "You have received an invitation" +msgstr "Du hast eine Einladung erhalten" + +#: app.py:1563 +msgid "Invitation successfully sent" +msgstr "Einladung erfolgreich gesendet" + +#: app.py:1592 +msgid "Member successfully updated" +msgstr "Mitglied erfolgreich aktualisiert" + +#: app.py:1618 +msgid "Entered email does not match member email" +msgstr "Die eingegebene Email passt nicht zur Email des Mitglieds" + +#: app.py:1623 +msgid "Member successfully deleted" +msgstr "Mitglied erfolgreich gelöscht" + +#: app.py:1650 +msgid "Entered email does not match invitation email" +msgstr "Die eingegebene Email passt nicht zur Email der Einladung" + +#: app.py:1655 +msgid "Invitation successfully deleted" +msgstr "Einladung erfolgreich gelöscht" + +#: app.py:1795 msgid "Organizer successfully created" msgstr "Veranstalter erfolgreich erstellt" -#: app.py:1484 +#: app.py:1817 msgid "Organizer successfully updated" msgstr "Veranstalter erfolgreich aktualisiert" -#: app.py:1506 +#: app.py:1840 msgid "Entered name does not match organizer name" msgstr "Der eingegebene Name entspricht nicht dem Namen des Veranstalters" -#: app.py:1511 +#: app.py:1845 msgid "Organizer successfully deleted" msgstr "Veranstalter erfolgreich gelöscht" @@ -204,12 +248,12 @@ msgstr "Längengrad" #: forms/admin_unit.py:19 forms/event.py:17 forms/event.py:35 forms/event.py:40 #: forms/event.py:117 forms/event_place.py:20 forms/organization.py:19 #: forms/organizer.py:19 forms/organizer.py:41 forms/place.py:19 -#: templates/_macros.html:104 templates/admin/admin_units.html:18 +#: templates/_macros.html:115 templates/admin/admin_units.html:18 #: templates/admin_unit/list.html:13 templates/admin_unit/read.html:66 #: templates/admin_unit/read.html:88 templates/event/list.html:17 #: templates/event_place/list.html:19 templates/organization/list.html:19 -#: templates/organization/read.html:64 templates/place/list.html:19 -#: templates/profile.html:15 templates/profile.html:37 +#: templates/place/list.html:19 templates/profile.html:16 +#: templates/profile.html:36 templates/profile.html:58 msgid "Name" msgstr "Name" @@ -220,6 +264,8 @@ msgstr "Kurzname" #: forms/admin_unit.py:20 msgid "The short name is used to create a unique identifier for your events" msgstr "Der Kurzname wird verwendet, um Ihre Veranstaltungen eindeutig identifizieren zu können" +"Der Kurzname wird verwendet, um Ihre Veranstaltungen eindeutig " +"identifizieren zu können" #: forms/admin_unit.py:20 forms/organization.py:25 msgid "Short name must contain only letters numbers or underscore" @@ -231,18 +277,20 @@ msgstr "Der Kurzname darf nur Buchstaben, Nummern und Unterstriche enthalten" msgid "Link URL" msgstr "Link URL" -#: forms/admin_unit.py:22 forms/event.py:30 forms/event.py:36 -#: forms/organization.py:21 forms/organizer.py:21 templates/_macros.html:210 +#: forms/admin_unit.py:22 forms/admin_unit_member.py:12 +#: forms/admin_unit_member.py:22 forms/admin_unit_member.py:26 +#: forms/event.py:30 forms/event.py:36 forms/organization.py:21 +#: forms/organizer.py:21 templates/_macros.html:221 msgid "Email" msgstr "Email" #: forms/admin_unit.py:23 forms/event.py:31 forms/event.py:37 -#: forms/organization.py:22 forms/organizer.py:22 templates/_macros.html:231 +#: forms/organization.py:22 forms/organizer.py:22 templates/_macros.html:242 msgid "Phone" msgstr "Telefon" #: forms/admin_unit.py:24 forms/event.py:32 forms/organization.py:23 -#: forms/organizer.py:23 templates/_macros.html:239 +#: forms/organizer.py:23 templates/_macros.html:250 msgid "Fax" msgstr "Fax" @@ -256,7 +304,7 @@ msgid "Images only!" msgstr "Nur Fotos!" #: forms/admin_unit.py:35 templates/admin_unit/create.html:10 -#: templates/manage/admin_units.html:8 +#: templates/manage/admin_units.html:18 msgid "Create admin unit" msgstr "Verwaltungseinheit erstellen" @@ -265,6 +313,36 @@ msgstr "Verwaltungseinheit erstellen" msgid "Update admin unit" msgstr "Verwaltungseinheit aktualisieren" +#: forms/admin_unit_member.py:13 forms/admin_unit_member.py:29 +#: templates/admin_unit/read.html:67 templates/admin_unit/read.html:89 +#: templates/profile.html:37 templates/profile.html:59 +msgid "Roles" +msgstr "Rollen" + +#: forms/admin_unit_member.py:14 +msgid "Invite" +msgstr "Einladen" + +#: forms/admin_unit_member.py:17 +msgid "Accept" +msgstr "Akzeptieren" + +#: forms/admin_unit_member.py:18 +msgid "Decline" +msgstr "Ablehnen" + +#: forms/admin_unit_member.py:21 templates/manage/delete_invitation.html:6 +msgid "Delete invitation" +msgstr "Einladung löschen" + +#: forms/admin_unit_member.py:25 templates/manage/delete_member.html:6 +msgid "Delete member" +msgstr "Mitglied löschen" + +#: forms/admin_unit_member.py:30 templates/admin_unit/update_member.html:9 +msgid "Update member" +msgstr "Mitglied aktualisieren" + #: forms/event.py:19 forms/event_place.py:33 forms/event_place.py:37 msgid "Other organizers can use this location" msgstr "Andere Veranstalter können diesen Ort verwenden" @@ -293,27 +371,27 @@ msgstr "Beginn" msgid "End" msgstr "Ende" -#: forms/event.py:47 templates/_macros.html:293 +#: forms/event.py:47 templates/_macros.html:304 msgid "Previous start date" msgstr "Vorheriges Startdatum" -#: forms/event.py:48 templates/_macros.html:192 +#: forms/event.py:48 templates/_macros.html:203 msgid "Tags" msgstr "Stichworte" #: forms/event.py:50 forms/event.py:138 forms/event_place.py:44 -#: templates/_macros.html:355 templates/event/create.html:59 +#: templates/_macros.html:366 templates/event/create.html:59 #: templates/event/update.html:46 templates/manage/events.html:18 #: templates/manage/places.html:18 templates/organizer/create.html:16 #: templates/organizer/delete.html:13 templates/organizer/update.html:16 msgid "Organizer" msgstr "Veranstalter" -#: forms/event.py:51 templates/_macros.html:311 +#: forms/event.py:51 templates/_macros.html:322 msgid "Category" msgstr "Kategorie" -#: forms/event.py:52 forms/organization.py:37 templates/_macros.html:370 +#: forms/event.py:52 forms/organization.py:37 templates/_macros.html:381 #: templates/admin_unit/create.html:16 templates/admin_unit/update.html:16 #: templates/event/update.html:91 templates/organization/create.html:58 msgid "Admin unit" @@ -372,7 +450,7 @@ msgid "Photo" msgstr "Foto" #: forms/event.py:72 forms/event.py:73 forms/event.py:104 -#: templates/_macros.html:325 templates/event/create.html:84 +#: templates/_macros.html:336 templates/event/create.html:84 #: templates/event/update.html:55 templates/event_place/create.html:20 #: templates/event_place/update.html:20 templates/place/create.html:20 #: templates/place/update.html:20 @@ -389,6 +467,7 @@ msgstr "Neuen Ort eingeben" #: forms/event.py:78 templates/event/create.html:31 templates/example.html:10 #: templates/manage/events.html:36 templates/manage/organizers.html:22 +#: templates/manage/widgets.html:20 templates/manage/widgets.html:23 msgid "Create event" msgstr "Veranstaltung erstellen" @@ -469,8 +548,8 @@ msgstr "Prüfung speichern" msgid "Find events" msgstr "Veranstaltungen finden" -#: forms/event.py:137 templates/manage/events.html:25 -#: templates/widget/event_date/list.html:31 +#: forms/event.py:137 templates/event_date/list.html:28 +#: templates/manage/events.html:25 templates/widget/event_date/list.html:31 msgid "Keyword" msgstr "Stichwort" @@ -500,8 +579,7 @@ msgstr "Offizieller Name" msgid "Create organization" msgstr "Organisation hinzufügen" -#: forms/organization.py:40 templates/organization/read.html:12 -#: templates/organization/update.html:10 +#: forms/organization.py:40 templates/organization/update.html:10 msgid "Update organization" msgstr "Organisation aktualisieren" @@ -519,16 +597,16 @@ msgstr "Veranstalter aktualisieren" msgid "Delete organizer" msgstr "Veranstalter löschen" -#: templates/_macros.html:103 templates/_macros.html:279 -#: templates/_macros.html:286 templates/event/list.html:16 +#: templates/_macros.html:114 templates/_macros.html:290 +#: templates/_macros.html:297 templates/event/list.html:16 msgid "Date" msgstr "Datum" -#: templates/_macros.html:105 templates/event/list.html:18 +#: templates/_macros.html:116 templates/event/list.html:18 msgid "Host" msgstr "Veranstalter" -#: templates/_macros.html:106 templates/_macros.html:246 +#: templates/_macros.html:117 templates/_macros.html:257 #: templates/admin_unit/create.html:26 templates/admin_unit/update.html:26 #: templates/event/list.html:19 templates/event_place/create.html:29 #: templates/event_place/update.html:29 templates/organization/create.html:27 @@ -538,72 +616,80 @@ msgstr "Veranstalter" msgid "Location" msgstr "Standort" -#: templates/_macros.html:117 templates/_macros.html:296 +#: templates/_macros.html:128 templates/_macros.html:307 #: templates/event/list.html:30 msgid "Verified" msgstr "Verifiziert" -#: templates/_macros.html:130 +#: templates/_macros.html:141 msgid "Show all events" msgstr "Alle Veranstaltungen anzeigen" -#: templates/_macros.html:146 +#: templates/_macros.html:157 msgid "Show on Google Maps" msgstr "Auf Google Maps anzeigen" -#: templates/_macros.html:201 +#: templates/_macros.html:212 msgid "Link" msgstr "Link" -#: templates/_macros.html:272 templates/event/create.html:38 +#: templates/_macros.html:283 templates/event/create.html:38 #: templates/event/delete.html:13 templates/event/update.html:15 msgid "Event" msgstr "Veranstaltung" -#: templates/_macros.html:282 +#: templates/_macros.html:293 #, python-format msgid "%(count)d event dates" msgstr "%(count)d Termine" -#: templates/_macros.html:345 +#: templates/_macros.html:356 msgid "Show directions" msgstr "Anreise planen" -#: templates/_macros.html:394 +#: templates/_macros.html:405 msgid "Sign in with Google" msgstr "Mit Google anmelden" -#: templates/_macros.html:454 +#: templates/_macros.html:465 msgid "Search location on Google" msgstr "Ort bei Google suchen" -#: templates/_macros.html:474 +#: templates/_macros.html:485 templates/manage/reviews.html:4 msgid "Reviews" msgstr "Prüfungen" -#: templates/_macros.html:475 templates/admin_unit/read.html:30 +#: templates/_macros.html:486 templates/admin_unit/read.html:30 #: templates/event/list.html:4 templates/event/list.html:8 -#: templates/event_place/read.html:22 templates/manage/events.html:4 -#: templates/manage/reviews.html:4 templates/organization/read.html:27 -#: templates/place/read.html:22 +#: templates/event_place/read.html:22 templates/layout.html:55 +#: templates/manage/events.html:4 templates/place/read.html:22 msgid "Events" msgstr "Veranstaltungen" -#: templates/_macros.html:476 templates/manage/organizers.html:4 +#: templates/_macros.html:487 templates/manage/organizers.html:4 msgid "Organizers" msgstr "Veranstalter" -#: templates/_macros.html:477 templates/event_place/list.html:3 +#: templates/_macros.html:488 templates/event_place/list.html:3 #: templates/event_place/list.html:7 templates/manage/places.html:4 #: templates/place/list.html:3 templates/place/list.html:7 msgid "Places" msgstr "Orte" -#: templates/_macros.html:517 templates/_macros.html:519 +#: templates/_macros.html:489 templates/admin_unit/read.html:23 +#: templates/manage/members.html:4 templates/manage/members.html:31 +msgid "Members" +msgstr "Mitglieder" + +#: templates/_macros.html:490 templates/manage/widgets.html:4 +msgid "Widgets" +msgstr "Widgets" + +#: templates/_macros.html:530 templates/_macros.html:532 msgid "Previous" msgstr "Zurück" -#: templates/_macros.html:522 templates/_macros.html:524 +#: templates/_macros.html:535 templates/_macros.html:537 msgid "Next" msgstr "Weiter" @@ -611,47 +697,51 @@ msgstr "Weiter" msgid "Widget als iFrame einbetten" msgstr "Widget als iFrame einbetten" +#: templates/home.html:25 templates/home.html:141 +#: templates/security/login_user.html:25 +msgid "Register for free" +msgstr "Kostenlos registrieren" + #: templates/layout.html:52 msgid "Manage" msgstr "Verwaltung" -#: templates/layout.html:55 +#: templates/layout.html:56 msgid "Example" msgstr "Beispiel" #: templates/developer/read.html:4 templates/developer/read.html:10 -#: templates/layout.html:56 +#: templates/layout.html:57 msgid "Developer" msgstr "Entwickler" -#: templates/layout.html:65 templates/profile.html:3 +#: templates/layout.html:66 templates/profile.html:4 msgid "Profile" msgstr "Profil" #: templates/admin/admin.html:3 templates/admin/admin.html:9 -#: templates/admin/admin_units.html:9 templates/layout.html:68 +#: templates/admin/admin_units.html:9 templates/layout.html:69 msgid "Admin" msgstr "Administration" -#: templates/layout.html:72 +#: templates/layout.html:73 msgid "Logout" msgstr "Ausloggen" +#: templates/manage/admin_units.html:8 templates/manage/members.html:12 +#: templates/profile.html:11 +msgid "Invitations" +msgstr "Einladungen" + #: templates/admin/admin.html:15 templates/admin/admin_units.html:3 #: templates/admin/admin_units.html:10 templates/admin_unit/list.html:3 #: templates/admin_unit/list.html:7 templates/manage/admin_units.html:3 -#: templates/profile.html:10 +#: templates/manage/admin_units.html:16 templates/profile.html:31 msgid "Admin Units" msgstr "Verwaltungseinheiten" -#: templates/admin_unit/read.html:67 templates/admin_unit/read.html:89 -#: templates/organization/read.html:65 templates/profile.html:16 -#: templates/profile.html:38 -msgid "Roles" -msgstr "Rollen" - #: templates/admin_unit/read.html:27 templates/organization/list.html:3 -#: templates/organization/list.html:7 templates/profile.html:32 +#: templates/organization/list.html:7 templates/profile.html:53 msgid "Organizations" msgstr "Organisationen" @@ -664,19 +754,36 @@ msgstr "Organisationen" msgid "Additional information" msgstr "Zusätzliche Informationen" +#: templates/admin_unit/invite_member.html:9 templates/manage/members.html:15 +msgid "Invite user" +msgstr "Nutzer:in einladen" + #: templates/admin_unit/read.html:19 templates/event_place/read.html:19 -#: templates/organization/read.html:19 templates/place/read.html:19 +#: templates/place/read.html:19 msgid "Info" msgstr "Info" -#: templates/admin_unit/read.html:23 templates/organization/read.html:23 -msgid "Members" -msgstr "Mitglieder" - #: templates/admin_unit/read.html:56 msgid "You are a member of this admin unit." msgstr "Du bist Mitglied dieser Verwaltungseinheit" +#: templates/email/invitation_notice.html:1 +#, python-format +msgid "You have been invited to join %(admin_unit_name)s." +msgstr "Du wurdest eingeladen, %(admin_unit_name)s beizutreten." + +#: templates/email/invitation_notice.html:2 +msgid "Click here to view the invitation" +msgstr "Klicke hier, um die Einladung anzunehmen." + +#: templates/email/layout.html:351 +msgid "Hi there" +msgstr "Moin" + +#: templates/email/layout.html:352 +msgid "this is a message from Oveda - Die offene Veranstaltungsdatenbank." +msgstr "das ist eine Nachricht von Oveda - Die offene Veranstaltungsdatenbank." + #: templates/event/create.html:48 templates/event/update.html:25 msgid "Event date" msgstr "Termin" @@ -711,16 +818,40 @@ msgstr "Sie können diese Seite erneut besuchen, um den Status zu prüfen." msgid "View" msgstr "Anzeigen" -#: templates/manage/events.html:30 templates/widget/event_date/list.html:36 +#: templates/event_date/list.html:14 templates/widget/event_date/list.html:17 +msgid "From" +msgstr "Von" + +#: templates/event_date/list.html:21 templates/widget/event_date/list.html:24 +msgid "to" +msgstr "bis" + +#: templates/event_date/list.html:33 templates/manage/events.html:30 +#: templates/widget/event_date/list.html:36 msgid "Find" msgstr "Finden" -#: templates/manage/events.html:47 templates/manage/organizers.html:23 -#: templates/manage/places.html:35 +#: templates/invitation/read.html:4 templates/invitation/read.html:8 +#: templates/manage/delete_invitation.html:13 +msgid "Invitation" +msgstr "Einladung" + +#: templates/invitation/read.html:10 +#, python-format +msgid "Would you like to accept the invitation from %(name)s?" +msgstr "Möchtest du die Einladung von %(name)s akzeptieren?" + +#: templates/manage/delete_member.html:13 +msgid "Member" +msgstr "Mitglied" + +#: templates/manage/events.html:47 templates/manage/members.html:38 +#: templates/manage/organizers.html:23 templates/manage/places.html:35 msgid "Edit" msgstr "Bearbeiten" -#: templates/manage/events.html:48 templates/manage/organizers.html:24 +#: templates/manage/events.html:48 templates/manage/members.html:24 +#: templates/manage/members.html:39 templates/manage/organizers.html:24 msgid "Delete" msgstr "Löschen" @@ -728,25 +859,29 @@ msgstr "Löschen" msgid "Assistents" msgstr "Assistenten" +#: templates/manage/widgets.html:12 +msgid "Veranstaltungen als iFrame einbetten" +msgstr "Veranstaltungen als iFrame einbetten" + +#: templates/manage/widgets.html:19 +msgid "Link, um Veranstaltungen vorzuschlagen" +msgstr "Link, um Veranstaltungen vorzuschlagen" + +#: templates/manage/widgets.html:26 +msgid "URL für Infoscreen" +msgstr "URL für Infoscreen" + #: templates/organization/create.html:16 templates/organization/update.html:16 msgid "Organization" msgstr "Organisation" -#: templates/organization/read.html:54 -msgid "You are a member of this organization." -msgstr "Du bist Mitglied dieser Organisation" +#: templates/security/login_user.html:23 +msgid "You do not have an account yet? Not a problem!" +msgstr "Du hast noch keinen Account? Kein Problem!" #: templates/widget/event_date/list.html:4 msgid "Widget" -msgstr "WIDGET" - -#: templates/widget/event_date/list.html:17 -msgid "From" -msgstr "Von" - -#: templates/widget/event_date/list.html:24 -msgid "to" -msgstr "bis" +msgstr "Widget" #~ msgid "You" #~ msgstr "Du" @@ -811,3 +946,6 @@ msgstr "bis" #~ msgid "Admin unit successfully updated" #~ msgstr "Verwaltungseinheit erfolgreich aktualisiert" +#~ msgid "You are a member of this organization." +#~ msgstr "Du bist Mitglied dieser Organisation" +