AU mit Mitglieder-Einladungen, die Events anlegen können für Vereine

This commit is contained in:
Daniel Grams 2020-09-26 13:30:42 +02:00
parent cbfc4a2669
commit 1f6ff9f2ee
24 changed files with 1306 additions and 146 deletions

303
app.py
View File

@ -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/<int:id>', 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/<int:id>')
@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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/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/<int:id>/places/create', methods=('GET', 'POST'))
@auth_required()
def manage_organizer_places_create(id):
organizer = EventOrganizer.query.get_or_404(id)

View File

@ -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"))

View File

@ -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):

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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))

View File

@ -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' %}
<fieldset class="form-group">
{% for choice in field %}
<div class="form-check">
{{ choice(class="form-check-input") }}
{{ choice.label(class="form-check-label") }}
</div>
{% endfor %}
</fieldset>
{% 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) }}
</ul>
{% endmacro %}
@ -527,4 +539,10 @@ $( function() {
</ul>
</nav>
{% endif %}
{% endmacro %}
{% macro render_roles(roles) %}
{% if roles %}
{% for role in roles %}{{ _(role.title) }}{%if not loop.last %}, {% endif %}{% endfor %}
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,24 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_manage_menu, render_field_with_errors, render_field %}
{% block content %}
<h1>{{ admin_unit.name }}</h1>
{{ render_manage_menu(admin_unit, 'members') }}
<h2>{{ _('Invite user') }}</h2>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-body">
{{ render_field_with_errors(form.email) }}
{{ render_field_with_errors(form.roles, ri="multicheckbox") }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -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 %}
<tr>
<td>{{ member.user.email }}</td>
<td>{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}</td>
<td>{{ render_roles(member.roles)}}</td>
</tr>
{% endfor %}
</tbody>
@ -93,7 +93,7 @@
{% for admin_unit_org in admin_unit.organizations %}
<tr>
<td><a href="{{ url_for('organization', organization_id=admin_unit_org.organization_id) }}">{{ admin_unit_org.organization.name }}</a></td>
<td>{% for role in admin_unit_org.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}</td>
<td>{{ render_roles(admin_unit_org.roles)}}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,25 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_manage_menu, render_field_with_errors, render_field %}
{% block content %}
<h1>{{ admin_unit.name }}</h1>
{{ render_manage_menu(admin_unit, 'members') }}
<h2>{{ _('Update member') }}</h2>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-header">
{{ member.user.email }}
</div>
<div class="card-body">
{{ render_field_with_errors(form.roles, ri="multicheckbox") }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "email/layout.html" %}
{% block content %}
<p>{{ _('You have been invited to join %(admin_unit_name)s.', admin_unit_name=invitation.adminunit.name) }}</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{{ url_for('admin_unit_member_invitation', id=invitation.id, _external=True) }}" target="_blank">{{ _('Click here to view the invitation') }}</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
{% endblock %}

View File

@ -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) }}

382
templates/email/layout.html Normal file
View File

@ -0,0 +1,382 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<table role="presentation" class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>{{ _('Hi there') }},</p>
<p>{{ _('this is a message from Oveda - Die offene Veranstaltungsdatenbank.') }}</p>
{% block content -%}
{%- endblock content %}
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
<!-- START FOOTER -->
<div class="footer">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
<span class="apple-link">Oveda - Die offene Veranstaltungsdatenbank</span>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -22,7 +22,7 @@ oveda - Offene Veranstaltungsdatenbank
<form>
<div class="form-row">
<div class="col-12">
<a class="btn btn-primary btn-lg" href="{{ url_for('security.register') }}" role="button">Kostenlos registrieren</a>
<a class="btn btn-primary btn-lg" href="{{ url_for('security.register') }}" role="button">{{ _('Register for free') }}</a>
</div>
</div>
</form>
@ -138,7 +138,7 @@ oveda - Offene Veranstaltungsdatenbank
<form>
<div class="form-row mb-4">
<div class="col-12">
<a class="btn btn-primary btn-lg" href="{{ url_for('security.register') }}" role="button">Kostenlos registrieren</a>
<a class="btn btn-primary btn-lg" href="{{ url_for('security.register') }}" role="button">{{ _('Register for free') }}</a>
</div>
</div>
<div class="form-row">

View File

@ -0,0 +1,27 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field %}
{% block title %}
{{ _('Invitation') }}
{% endblock %}
{% block content %}
<h1>{{ _('Invitation') }}</h1>
<p>{{ _('Would you like to accept the invitation from %(name)s?', name=invitation.adminunit.name) }}</p>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="container">
<div class="row">
<div class="col-auto">
{{ render_field(form.accept, class='btn btn-success') }}
</div>
<div class="col-auto">
{{ render_field(form.decline, class='btn btn-secondary') }}
</div>
</div>
</div>
</form>
{% endblock %}

View File

@ -4,11 +4,21 @@
{% endblock %}
{% block content %}
{% if invitations %}
<h2>{{ _('Invitations') }}</h2>
<div class="list-group">
{% for invitation in invitations %}
<a href="{{ url_for('admin_unit_member_invitation', id=invitation.id) }}" class="list-group-item list-group-item-action">{{ invitation.adminunit.name }}</a>
{% endfor %}
</div>
{% endif %}
<h2>{{ _('Admin Units') }}</h2>
<div class="my-4">
<a class="btn btn-outline-secondary my-1" href="{{ url_for('admin_unit_create') }}" role="button"><i class="fa fa-plus"></i> {{ _('Create admin unit') }}</a>
</div>
<div class="list-group list-group-flush">
<div class="list-group">
{% for admin_unit in admin_units %}
<a href="{{ url_for('manage_admin_unit', id=admin_unit.id) }}" class="list-group-item list-group-item-action">{{ admin_unit.name }}</a>
{% endfor %}

View File

@ -0,0 +1,24 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field_with_errors, render_field %}
{% block content %}
<h1>{{ _('Delete invitation') }} &quot;{{ invitation.email }}&quot;</h1>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-header">
{{ _('Invitation') }}
</div>
<div class="card-body">
{{ render_field_with_errors(form.email) }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field_with_errors, render_field %}
{% block content %}
<h1>{{ _('Delete member') }} &quot;{{ member.user.email }}&quot;</h1>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-header">
{{ _('Member') }}
</div>
<div class="card-body">
{{ render_field_with_errors(form.email) }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -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 %}
<h1>{{ admin_unit.name }}</h1>
{{ render_manage_menu(admin_unit, 'members') }}
<h2>{{ _('Invitations') }}</h2>
<div class="my-4">
{% if can_invite_users %}
<a class="btn btn-outline-secondary my-1" href="{{ url_for('manage_admin_unit_member_invite', id=admin_unit.id) }}" role="button"><i class="fa fa-plus"></i> {{ _('Invite user') }}</a>
{% endif %}
</div>
<ul class="list-group">
{% for invitation in invitations %}
<li class="list-group-item">
<div class="dropdown d-inline-block">
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ invitation.email }}</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_invitation_delete', id=invitation.id) }}">{{ _('Delete') }}...</a>
</div>
</div>
</li>
{% endfor %}
</ul>
<h2>{{ _('Members') }}</h2>
<ul class="list-group">
{% for member in members %}
<li class="list-group-item">
<div class="dropdown d-inline-block">
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ member.user.email }}</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_member_update', id=member.id) }}">{{ _('Edit') }}</a>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_member_delete', id=member.id) }}">{{ _('Delete') }}...</a>
</div>
</div>
<small>{{ render_roles(member.roles)}}</small>
</li>
{% endfor %}
</ul>
<div class="my-4">{{ render_pagination(pagination) }}</div>
{% endblock %}

View File

@ -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 %}
<tr>
<td>{{ member.user.email }}</td>
<td>{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}</td>
<td>{{ render_roles(member.roles)}}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -1,4 +1,5 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_roles %}
{% block title %}
{{ _('Profile') }}
{% endblock %}
@ -6,6 +7,26 @@
<h1>{{ current_user.email }}</h1>
{% if invitations %}
<h2>{{ _('Invitations') }}</h2>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover table-striped">
<thead>
<tr>
<th>{{ _('Name') }}</th>
</tr>
</thead>
<tbody>
{% for invitation in invitations %}
<tr>
<td><a href="{{ url_for('admin_unit_member_invitation', id=invitation.id) }}">{{ invitation.adminunit.name }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if admin_unit_members %}
<h2>{{ _('Admin Units') }}</h2>
<div class="table-responsive">
@ -19,8 +40,8 @@
<tbody>
{% for member in admin_unit_members %}
<tr>
<td><a href="{{ url_for('admin_unit', admin_unit_id=member.adminunit.id) }}">{{ member.adminunit.name }}</a></td>
<td>{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}</td>
<td><a href="{{ url_for('manage_admin_unit', id=member.adminunit.id) }}">{{ member.adminunit.name }}</a></td>
<td>{{ render_roles(member.roles)}}</td>
</tr>
{% endfor %}
</tbody>
@ -42,7 +63,7 @@
{% for member in organization_members %}
<tr>
<td>{{ member.organization.name }}</td>
<td>{% for role in member.roles %}{{ role.name }}{%if not loop.last %}, {% endif %}{% endfor %}</td>
<td>{{ render_roles(member.roles)}}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -18,8 +18,16 @@
{{ render_field(login_user_form.submit) }}
</form>
<hr class="my-4">
{{ render_google_sign_in_button() }}
<div class="card" class="my-4">
<div class="card-body">
<p class="card-text">{{ _('You do not have an account yet? Not a problem!') }}</p>
<div class="my-2">
<a href="{{ url_for_security('register') }}" class="btn btn-dark"><i class="fa fa-user-plus mr-2"></i> {{ _('Register for free') }}</a>
</div>
<div class="my-2">
{{ render_google_sign_in_button() }}
</div>
</div>
</div>
{% endblock %}

View File

@ -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 <EMAIL@ADDRESS>\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"