Compare commits

..

17 Commits

Author SHA1 Message Date
Raphael Michel
b2cd633248 Update django-bootstrap3, bump version 2017-01-07 17:23:41 +01:00
Raphael Michel
0acee0e362 Get rid of User.givenname and User.familyname 2017-01-07 16:35:04 +01:00
Raphael Michel
33265d05fb Add event_live_issues signal 2017-01-07 14:41:53 +01:00
Raphael Michel
2182a4e361 Add improved UI if no event can be created 2017-01-07 14:25:01 +01:00
Raphael Michel
2336505309 Replace "Slug" with "Short form" for organizers 2017-01-07 14:24:38 +01:00
Raphael Michel
15b5e66da9 Add URL to permission test list 2017-01-07 14:13:14 +01:00
Raphael Michel
c7676cd17a Refs #39 -- Add permission editor for organizers 2017-01-07 14:10:31 +01:00
Raphael Michel
e53562dda2 Updated German translation 2017-01-07 13:14:21 +01:00
Raphael Michel
d134dcf6a9 Added team invitations 2017-01-07 13:05:36 +01:00
Raphael Michel
981d82b0ee Updated German translation 2017-01-07 11:39:55 +01:00
Raphael Michel
e75bce37bc Fix incorrect paths in Makefile 2017-01-05 13:08:40 +01:00
Raphael Michel
ef432252f0 Fix invalid URL usage 2017-01-05 12:20:58 +01:00
Raphael Michel
e9e743f312 Add more log texts 2017-01-05 12:20:18 +01:00
Raphael Michel
0998814e69 Improve session inheritation 2017-01-05 12:15:38 +01:00
Raphael Michel
d3f21353ca Allow to access not-yet-live shop on different domain 2017-01-05 12:11:50 +01:00
Raphael Michel
f6d8b825d5 Add plugin installation docs and rebuild command 2017-01-04 23:33:07 +01:00
Raphael Michel
4012658596 Add PyPI badges 2017-01-04 22:45:38 +01:00
50 changed files with 1918 additions and 528 deletions

View File

@@ -1,6 +1,9 @@
pretix
======
.. image:: https://img.shields.io/pypi/v/pretix.svg
:target: https://pypi.python.org/pypi/pretix
.. image:: https://readthedocs.org/projects/pretix/badge/?version=latest
:target: https://docs.pretix.eu/en/latest/

View File

@@ -33,7 +33,7 @@ fi
if [ "$1" == "taskworker" ]; then
export C_FORCE_ROOT=True
exec celery -A pretix worker -l info
exec celery -A pretix.celery_app worker -l info
fi
if [ "$1" == "shell" ]; then

View File

@@ -235,6 +235,26 @@ Updates are fairly simple, but require at least a short downtime::
Restarting the service can take a few seconds, especially if the update requires changes to the database.
Install a plugin
----------------
To install a plugin, you need to build your own docker image. To do so, create a new directory and place a file
named ``Dockerfile`` in it. The Dockerfile could look like this (replace ``pretix-passbook`` with the plugins of your
choice)::
FROM pretix/standalone
USER root
RUN pip3 install pretix-passbook
USER pretixuser
RUN make production
Then, go to that directory and build the image::
$ docker build -t mypretix
You can now use that image ``mypretix`` instead of ``pretix/standalone`` in your service file (see above). Be sure
to re-build your custom image after you pulled ``pretix/standalone`` if you want to perform an update.
.. _Docker: https://docs.docker.com/engine/installation/linux/debian/
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/

View File

@@ -105,8 +105,8 @@ Fill the configuration file ``/etc/pretix/pretix.cfg`` with the following conten
See :ref:`email configuration <mail-settings>` to learn more about configuring mail features.
Install pretix from source
--------------------------
Install pretix from PyPI
------------------------
Now we will install pretix itself. The following steps are to be executed as the ``pretix`` user. Before we
actually install pretix, we will create a virtual environment to isolate the python packages from your global
@@ -116,14 +116,10 @@ python installation::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install -U pip setuptools wheel
We now clone pretix and install its Python dependencies (replace ``mysql`` with ``postgres`` if you're running
PostgreSQL)::
We now install pretix, its direct dependencies and gunicorn. Replace ``mysql`` with ``postgres`` in the following
command if you're running PostgreSQL::
(venv)$ git clone https://github.com/pretix/pretix.git /var/pretix/source
(venv)$ cd /var/pretix/source/src
(venv)$ pip3 install -r requirements.txt -r requirements/mysql.txt \
-r requirements/redis.txt \
-r requirements/py34.txt gunicorn
(venv)$ pip install "pretix[mysql]" gunicorn
We also need to create a data directory::
@@ -131,8 +127,8 @@ We also need to create a data directory::
Finally, we compile static files and translation data and create the database structure::
(venv)$ make production
(venv)$ python manage.py migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix migrate
Start pretix as a service
@@ -171,7 +167,7 @@ For background tasks we need a second service ``/etc/systemd/system/pretix-worke
Group=pretix
Environment="VIRTUAL_ENV=/var/pretix/venv"
Environment="PATH=/var/pretix/venv/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/var/pretix/venv/bin/celery -A pretix worker -l info
ExecStart=/var/pretix/venv/bin/celery -A pretix.celery_app worker -l info
WorkingDirectory=/var/pretix/source/src
Restart=on-failure
@@ -191,7 +187,7 @@ Cronjob
You need to set up a cronjob that runs the management command ``runperiodic``. The exact interval is not important
but should be something between every minute and every hour. You could for example configure cron like this::
15,45 * * * * export PATH=/var/pretix/venv/bin:$PATH && cd /var/pretix/source/src && ./manage.py runperiodic
15,45 * * * * export PATH=/var/pretix/venv/bin:$PATH && cd /var/pretix/source/src && python -m pretix runperiodic
The cronjob should run as the ``pretix`` user (``crontab -e -u pretix``).
@@ -254,16 +250,27 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
``mysql`` with ``postgres`` if necessary)::
$ source /var/pretix/venv/bin/activate
(venv)$ cd /var/pretix/source/src
(venv)$ git pull origin master
(venv)$ pip3 install -r requirements.txt -r requirements/mysql.txt \
-r requirements/redis.txt \
-r requirements/py34.txt gunicorn
(venv)$ python manage.py migrate
(venv)$ make production
(venv)$ python manage.py updatestyles
(venv)$ pip3 install -U pretix[mysql] gunicorn
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Install a plugin
----------------
To install a plugin, just use ``pip``! Depending on the plugin, you should probably apply database migrations and
rebuild the static files afterwards. Replace ``pretix-passbook`` with the plugin of your choice in the following
example::
$ source /var/pretix/venv/bin/activate
(venv)$ pip3 install pretix-passbook
(venv)$ python -m pretix migrate
(venv)$ python -m pretix rebuild
# systemctl restart pretix-web pretix-worker
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
.. _Let's Encrypt: https://letsencrypt.org/

View File

@@ -11,7 +11,7 @@ Core
----
.. automodule:: pretix.base.signals
:members: periodic_task
:members: periodic_task, event_live_issues
Order events
""""""""""""

View File

@@ -6,7 +6,7 @@ localecompile:
localegen:
./manage.py makemessages --all --ignore "pretix/helpers/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "static/jsi18n/*"
./manage.py makemessages --all -d djangojs --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "build/*"
staticfiles: jsi18n
./manage.py collectstatic --noinput

View File

@@ -1 +1 @@
__version__ = "1.0.0b1"
__version__ = "1.0.0b2"

View File

@@ -18,3 +18,7 @@ class PretixBaseConfig(AppConfig):
default_app_config = 'pretix.base.PretixBaseConfig'
try:
import pretix.celery_app as celery # NOQA
except ImportError:
pass

View File

@@ -39,8 +39,7 @@ class UserSettingsForm(forms.ModelForm):
class Meta:
model = User
fields = [
'givenname',
'familyname',
'fullname',
'locale',
# 'timezone',
'email'

View File

@@ -0,0 +1,15 @@
from django.core.management import call_command
from django.core.management.base import BaseCommand
from ...signals import periodic_task
class Command(BaseCommand):
help = "Rebuild static files and language files"
def handle(self, *args, **options):
periodic_task.send(self)
call_command('compilemessages', verbosity=1, interactive=False)
call_command('compilejsi18n', verbosity=1, interactive=False)
call_command('collectstatic', verbosity=1, interactive=False)
call_command('compress', verbosity=1, interactive=False)

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 10:58
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.event
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0053_auto_20170104_1252'),
]
operations = [
migrations.AddField(
model_name='eventpermission',
name='invite_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='eventpermission',
name='invite_token',
field=models.CharField(blank=True, default=pretix.base.models.event.generate_invite_token, max_length=64, null=True),
),
migrations.AlterField(
model_name='eventpermission',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='event_perms', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 12:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0054_auto_20170107_1058'),
]
operations = [
migrations.AddField(
model_name='organizerpermission',
name='can_change_permissions',
field=models.BooleanField(default=True, verbose_name='Can change permissions'),
),
]

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 12:51
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.organizer
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0055_organizerpermission_can_change_permissions'),
]
operations = [
migrations.AddField(
model_name='organizerpermission',
name='invite_email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AddField(
model_name='organizerpermission',
name='invite_token',
field=models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True),
),
migrations.AlterField(
model_name='organizerpermission',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organizer_perms', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 15:31
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import pretix.base.validators
def merge_names(apps, schema_editor):
User = apps.get_model('pretixbase', 'User')
for u in User.objects.all():
if u.givenname:
if u.familyname:
u.fullname = u.givenname + " " + u.familyname
else:
u.fullname = u.givenname
elif u.familyname:
u.fullname = u.familyname
u.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0056_auto_20170107_1251'),
]
operations = [
migrations.AddField(
model_name='user',
name='fullname',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Full name'),
),
migrations.AlterField(
model_name='organizer',
name='slug',
field=models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'), pretix.base.validators.OrganizerSlugBlacklistValidator()], verbose_name='Short form'),
),
migrations.RunPython(merge_names, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-01-07 15:33
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0057_auto_20170107_1531'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='familyname',
),
migrations.RemoveField(
model_name='user',
name='givenname',
),
]

View File

@@ -3,6 +3,7 @@ from .base import CachedFile, LoggedModel, cachedfile_name
from .checkin import Checkin
from .event import (
Event, EventLock, EventPermission, EventSetting, RequiredAction,
generate_invite_token,
)
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (

View File

@@ -43,10 +43,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:param email: The user's email address, used for identification.
:type email: str
:param givenname: The user's given name. May be empty or null.
:type givenname: str
:param familyname: The user's given name. May be empty or null.
:type familyname: str
:param fullname: The user's full name. May be empty or null.
:type fullname: str
:param is_active: Whether this user account is activated.
:type is_active: bool
:param is_staff: ``True`` for system operators.
@@ -64,10 +62,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('E-mail'))
givenname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Given name'))
familyname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Family name'))
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
verbose_name=_('Is active'))
is_staff = models.BooleanField(default=False,
@@ -100,14 +96,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
Returns the first of the following user properties that is found to exist:
* Given name
* Family name
* Full name
* Email address
Only present for backwards compatibility
"""
if self.givenname:
return self.givenname
elif self.familyname:
return self.familyname
if self.fullname:
return self.fullname
else:
return self.email
@@ -115,20 +110,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
Returns the first of the following user properties that is found to exist:
* A combination of given name and family name, depending on the locale
* Given name
* Family name
* User name
* Full name
* Email address
"""
if self.givenname and not self.familyname:
return self.givenname
elif not self.givenname and self.familyname:
return self.familyname
elif self.familyname and self.givenname:
return _('%(family)s, %(given)s') % {
'family': self.familyname,
'given': self.givenname
}
if self.fullname:
return self.fullname
else:
return self.email

View File

@@ -1,3 +1,4 @@
import string
import uuid
from datetime import date, datetime, time
@@ -294,6 +295,10 @@ class Event(LoggedModel):
s.save()
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
class EventPermission(models.Model):
"""
The relation between an Event and a User who has permissions to
@@ -314,7 +319,9 @@ class EventPermission(models.Model):
"""
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True)
invite_email = models.EmailField(null=True, blank=True)
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
can_change_settings = models.BooleanField(
default=True,
verbose_name=_("Can change event settings")

View File

@@ -101,7 +101,7 @@ class LogEntry(models.Model):
elif isinstance(co, ItemCategory):
a_text = _('Category {val}')
a_map = {
'href': reverse('control:event.items.categories.show', kwargs={
'href': reverse('control:event.items.categories.edit', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
'category': co.id

View File

@@ -1,5 +1,8 @@
import string
from django.core.validators import RegexValidator
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
@@ -38,7 +41,7 @@ class Organizer(LoggedModel):
),
OrganizerSlugBlacklistValidator()
],
verbose_name=_("Slug"),
verbose_name=_("Short form"),
)
permitted = models.ManyToManyField(User, through='OrganizerPermission',
related_name="organizers")
@@ -76,6 +79,10 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self)
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
class OrganizerPermission(models.Model):
"""
The relation between an Organizer and a User who has permissions to
@@ -91,11 +98,17 @@ class OrganizerPermission(models.Model):
"""
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="organizer_perms")
user = models.ForeignKey(User, related_name="organizer_perms", on_delete=models.CASCADE, null=True, blank=True)
invite_email = models.EmailField(null=True, blank=True)
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
can_create_events = models.BooleanField(
default=True,
verbose_name=_("Can create events"),
)
can_change_permissions = models.BooleanField(
default=True,
verbose_name=_("Can change permissions"),
)
class Meta:
verbose_name = _("Organizer permission")

View File

@@ -52,6 +52,18 @@ class EventPluginSignal(django.dispatch.Signal):
return responses
event_live_issues = EventPluginSignal(
providing_args=[]
)
"""
This signal is sent out to determine whether an event can be taken live. If you want to
prevent the event from going live, return a string that will be displayed to the user
as the error message. If you don't, your receiver should return ``None``.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_payment_providers = EventPluginSignal(
providing_args=[]
)

View File

@@ -109,6 +109,14 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.plugins.disabled': _('A plugin has been disabled.'),
'pretix.event.live.activated': _('The shop has been taken live.'),
'pretix.event.live.deactivated': _('The shop has been taken offline.'),
'pretix.event.changed': _('The event settings have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'),
'pretix.event.permissions.added': _('A user has been added to the event team.'),
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
'pretix.event.permissions.deleted': _('A user has been removed from the event team.'),
}
data = json.loads(logentry.data)

View File

@@ -9,7 +9,9 @@ from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_str
from django.utils.translation import ugettext as _
from pretix.base.models import Event, EventPermission, Organizer
from pretix.base.models import (
Event, EventPermission, Organizer, OrganizerPermission,
)
class PermissionMiddleware(MiddlewareMixin):
@@ -24,7 +26,8 @@ class PermissionMiddleware(MiddlewareMixin):
"auth.login.2fa",
"auth.register",
"auth.forgot",
"auth.forgot.recover"
"auth.forgot.recover",
"auth.invite",
)
def process_request(self, request):
@@ -81,6 +84,10 @@ class PermissionMiddleware(MiddlewareMixin):
slug=url.kwargs['organizer'],
permitted__id__exact=request.user.id,
)[0]
request.orgaperm = OrganizerPermission.objects.get(
organizer=request.organizer,
user=request.user
)
except IndexError:
raise Http404(_("The selected organizer was not found or you "
"have no permission to administrate it."))

View File

@@ -0,0 +1,31 @@
{% extends "pretixcontrol/auth/base.html" %}
{% load bootstrap3 %}
{% load staticfiles %}
{% load i18n %}
{% block content %}
<form class="form-signin" action="" method="post">
<h3>{% trans "Accept an invitation" %}</h3>
<p>
{% url "control:auth.login" as loginurl %}
{% blocktrans trimmed with login_href='href="'|add:loginurl|add:'"'|safe %}
If you already have an account on this site with a different email address, you can
<a {{ login_href }}>log in</a> first and then click this link again to accept the
invitation with your existing account.
{% endblocktrans %}
</p>
{% bootstrap_form_errors form type='all' layout='inline' %}
{% csrf_token %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
{% bootstrap_field form.password_repeat %}
<div class="form-group buttons">
<a href="{% url "control:auth.login" %}" class="btn btn-link">
&laquo; {% trans "Login" %}
</a>
<button type="submit" class="btn btn-primary">
{% trans "Register" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to the team of an event that uses pretix for their
ticket sales.
Event: {{ event }}
If you want to join that team, just click on the following link:
{{ url }}
If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
{% endblocktrans %}

View File

@@ -0,0 +1,16 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to the team of an event organizer that uses pretix
for their ticket sales.
Organizer: {{ organizer }}
If you want to join that team, just click on the following link:
{{ url }}
If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
{% endblocktrans %}

View File

@@ -3,12 +3,21 @@
{% load eventurl %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>
{{ request.event.name }}
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-default btn-sm" target="_blank">
{% trans "Go to shop" %}
</a>
</h1>
<form action="{% eventurl request.event "presale:event.auth" %}" method="post" target="_blank">
<h1>
{{ request.event.name }}
{% if has_domain and not request.event.live %}
<input type="hidden" value="{{ new_session }}" name="session">
<button type="submit" class="btn btn-default btn-sm">
{% trans "Go to shop" %}
</button>
{% else %}
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-default btn-sm" target="_blank">
{% trans "Go to shop" %}
</a>
{% endif %}
</h1>
</form>
{% if actions|length > 0 %}
<div class="panel panel-danger">

View File

@@ -26,7 +26,16 @@
<tbody>
{% for form in formset %}
<tr>
<td>{{ form.id }}{{ form.instance.user }}</td>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_change_settings }}</td>
<td>{{ form.can_change_items }}</td>
<td>{{ form.can_view_orders }}</td>
@@ -37,6 +46,19 @@
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% blocktrans trimmed %}
To add a new user, you can enter their email address here. If they already have a
pretix account, they will immediately be added to the event. Otherwise, they will
be sent an email with an invitation.
{% endblocktrans %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
@@ -53,7 +75,7 @@
<td>{{ add_form.can_change_vouchers }}</td>
<td>{{ add_form.can_view_vouchers }}</td>
</tr>
</tbody>
</tfoot>
</table>
</div>
</fieldset>

View File

@@ -6,22 +6,33 @@
<h1>{% trans "Create a new event" %} <small>{% blocktrans trimmed with step=wizard.steps.step1 %}
Step {{ step }}
{% endblocktrans %}</small></h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{{ wizard.management_form }}
{% bootstrap_form_errors form %}
{% block form %}
{% endblock %}
<div class="form-group submit-group">
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
class="btn btn-default btn-lg pull-left">
{% trans "Back" %}
{% if has_organizer %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{{ wizard.management_form }}
{% bootstrap_form_errors form %}
{% block form %}
{% endblock %}
<div class="form-group submit-group">
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}"
class="btn btn-default btn-lg pull-left">
{% trans "Back" %}
</button>
{% endif %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
{% endif %}
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
</button>
</div>
</form>
{% else %}
<div class="alert alert-danger">
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
</div>
</form>
{% if request.user.is_superuser %}
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
{% trans "Create a new organizer" %}
</a>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -3,19 +3,131 @@
{% load bootstrap3 %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizer" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
<h1>
{% blocktrans with name=organizer.name %}Organizer: {{ name }}{% endblocktrans %}
<a href="{% url "control:organizer.edit" organizer=organizer.slug %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
</h1>
<div class="row">
<div class="{% if request.orgaperm.can_change_permissions %}col-lg-6{% endif %} col-xs-12">
<fieldset>
<legend>{% trans "Events" %}</legend>
{% if events|length == 0 %}
<p>
<em>{% trans "You currently do not have access to any events." %}</em>
</p>
{% else %}
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Start date" %}</th>
</tr>
</thead>
<tbody>
{% for e in events %}
<tr>
<td>
<strong><a href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
</td>
<td>{{ e.get_date_from_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<a href="{% url "control:events.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new event" %}
</a>
</fieldset>
</div>
</form>
{% if request.orgaperm.can_change_permissions %}
<div class="col-lg-6 col-xs-12">
<form action="" method="post" class="form-horizontal form-permissions">
{% csrf_token %}
<fieldset>
<legend>{% trans "Team" %}</legend>
<p>
{% blocktrans trimmed %}
You can use the following list to control who can create new events in the name of this
organizer and who can add more people to this list. This does <strong>not</strong>
control who has access to a particular event. You can control the access to an
event in the "Permissions" section of the event's settings. A user does not need to be
on the list here to get access to an event.
{% endblocktrans %}
</p>
{% bootstrap_formset_errors formset %}
{{ formset.management_form }}
<div class="table-responsive">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>{% trans "User" %}</th>
<th>{% trans "Create events" %}</th>
<th>{% trans "Change permissions" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
<td>
{{ form.id }}
{% if form.instance.user %}
{{ form.instance.user }}
{% else %}
{{ form.instance.invite_email }}
<span class="fa fa-envelope-o" data-toggle="tooltip"
title="{% trans "invited, pending response" %}"></span>
{% endif %}
</td>
<td>{{ form.can_create_events }}</td>
<td>{{ form.can_change_permissions }}</td>
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="9">
<strong>{% trans "Adding a new user" %}</strong><br>
{% blocktrans trimmed %}
To add a new user, you can enter their email address here. If they already have a
pretix account, they will immediately be added to the team. Otherwise, they will
be sent an email with an invitation.
{% endblocktrans %}
</td>
</tr>
<tr>
<td>
<div class="row-fluid">
<div class="col-sm-12">
{% bootstrap_field add_form.user layout='inline' %}
</div>
</div>
</td>
<td>{{ add_form.can_create_events }}</td>
<td>{{ add_form.can_change_permissions }}</td>
</tr>
</tfoot>
</table>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</fieldset>
</form>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %}
<h1>{% trans "Organizer" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.slug layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -20,7 +20,7 @@
{% for o in organizers %}
<tr>
<td><strong>
<a href="{% url "control:organizer.edit" organizer=o.slug %}">{{ o.name }}</a>
<a href="{% url "control:organizer" organizer=o.slug %}">{{ o.name }}</a>
</strong></td>
</tr>
{% endfor %}

View File

@@ -9,8 +9,7 @@
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "General settings" %}</legend>
{% bootstrap_field form.givenname layout='horizontal' %}
{% bootstrap_field form.familyname layout='horizontal' %}
{% bootstrap_field form.fullname layout='horizontal' %}
{% bootstrap_field form.locale layout='horizontal' %}
</fieldset>
<fieldset>

View File

@@ -10,6 +10,7 @@ urlpatterns = [
url(r'^login$', auth.login, name='auth.login'),
url(r'^login/2fa$', auth.Login2FAView.as_view(), name='auth.login.2fa'),
url(r'^register$', auth.register, name='auth.register'),
url(r'^invite/(?P<token>[a-zA-Z0-9]+)$', auth.invite, name='auth.invite'),
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
url(r'^$', dashboards.user_index, name='index'),
@@ -31,6 +32,7 @@ urlpatterns = [
name='user.settings.2fa.delete'),
url(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
url(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^events/$', main.EventList.as_view(), name='events'),
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),

View File

@@ -23,7 +23,9 @@ from u2flib_server.utils import rand_bytes
from pretix.base.forms.auth import (
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
)
from pretix.base.models import U2FDevice, User
from pretix.base.models import (
EventPermission, OrganizerPermission, U2FDevice, User,
)
from pretix.base.services.mail import SendMailException, mail
from pretix.helpers.urls import build_absolute_uri
@@ -99,6 +101,68 @@ def register(request):
return render(request, 'pretixcontrol/auth/register.html', ctx)
def invite(request, token):
"""
Registration form in case of an invite
"""
ctx = {}
try:
perm = EventPermission.objects.get(invite_token=token)
desc = perm.event.name
except EventPermission.DoesNotExist:
try:
perm = OrganizerPermission.objects.get(invite_token=token)
desc = perm.organizer.name
except OrganizerPermission.DoesNotExist:
messages.error(request, _('You used an invalid link. Please copy the link from your email to the address bar '
'and make sure it is correct and that the link has not been used before.'))
return redirect('control:auth.login')
if request.user.is_authenticated:
try:
if isinstance(perm, EventPermission):
EventPermission.objects.get(event=perm.event, user=request.user)
else:
OrganizerPermission.objects.get(organizer=perm.organizer, user=request.user)
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
'this team.').format(desc))
return redirect('control:index')
except (EventPermission.DoesNotExist, OrganizerPermission.DoesNotExist):
pass
perm.invite_token = None
perm.invite_email = None
perm.user = request.user
perm.save()
messages.success(request, _('You have now access to "{}".').format(desc))
return redirect('control:index')
if request.method == 'POST':
form = RegistrationForm(data=request.POST)
if form.is_valid():
user = User.objects.create_user(
form.cleaned_data['email'], form.cleaned_data['password'],
locale=request.LANGUAGE_CODE,
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE
)
user = authenticate(email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user)
request.session['pretix_auth_login_time'] = int(time.time())
perm.invite_token = None
perm.invite_email = None
perm.user = user
perm.save()
messages.success(request, _('Welcome to pretix! You have now access to "{}".').format(desc))
return redirect('control:index')
else:
form = RegistrationForm(initial={'email': perm.invite_email})
ctx['form'] = form
return render(request, 'pretixcontrol/auth/invite.html', ctx)
class Forgot(TemplateView):
template_name = 'pretixcontrol/auth/forgot.html'

View File

@@ -1,5 +1,7 @@
from decimal import Decimal
from importlib import import_module
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db.models import Sum
@@ -17,6 +19,7 @@ from pretix.control.signals import (
from ..logdisplay import OVERVIEW_BLACKLIST
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
@@ -167,11 +170,22 @@ def event_index(request, organizer, event):
a_qs = request.event.requiredaction_set.filter(done=False)
has_domain = request.event.organizer.domains.exists()
ctx = {
'widgets': rearrange(widgets),
'logs': qs[:5],
'actions': a_qs[:5] if request.eventperm.can_change_orders else []
'actions': a_qs[:5] if request.eventperm.can_change_orders else [],
'has_domain': has_domain
}
if not request.event.live and has_domain:
s = SessionStore()
s['pretix_event_access_{}'.format(request.event.pk)] = request.session.session_key
s.create()
ctx['new_session'] = s.session_key
request.session['event_access'] = True
for a in ctx['actions']:
a.display = a.display(request)

View File

@@ -22,8 +22,9 @@ from pretix.base.models import (
)
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
event_live_issues, register_payment_providers, register_ticket_outputs,
)
from pretix.control.forms.event import (
DisplaySettingsForm, EventSettingsForm, EventUpdateForm,
@@ -31,6 +32,7 @@ from pretix.control.forms.event import (
TicketSettingsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
from pretix.presale.style import regenerate_css
from . import UpdateView
@@ -544,27 +546,57 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
ctx['add_form'] = self.add_form
return ctx
def _send_invite(self, instance):
try:
mail(
instance.invite_email,
_('Account information changed'),
'pretixcontrol/email/invitation.txt',
{
'user': self,
'event': self.request.event.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.invite_token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
try:
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
self.add_form.instance.event = self.request.event
self.add_form.instance.event_id = self.request.event.id
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
except User.DoesNotExist:
messages.error(self.request, _('There is no user with the email address you entered.'))
return self.get(*args, **kwargs)
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
if EventPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
event=self.request.event).exists():
messages.error(self.request, _('This user already has been invited for this event.'))
return self.get(*args, **kwargs)
self.add_form.save()
self._send_invite(self.add_form.instance)
self.request.event.log_action(
'pretix.event.permissions.invited', user=self.request.user, data=logdata
)
else:
if EventPermission.objects.filter(user=self.add_form.instance.user,
event=self.request.event).exists():
messages.error(self.request, _('This user already has permissions for this event.'))
return self.get(*args, **kwargs)
self.add_form.save()
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
logdata['user'] = self.add_form.instance.user_id
self.request.event.log_action(
'pretix.event.permissions.added', user=self.request.user, data=logdata
@@ -583,6 +615,14 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
return self.get(*args, **kwargs)
for form in self.formset.deleted_forms:
logdata = {
k: v for k, v in form.cleaned_data.items()
}
self.request.event.log_action(
'pretix.event.permissions.deleted', user=self.request.user, data=logdata
)
self.formset.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
@@ -628,6 +668,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
if not self.request.event.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
responses = event_live_issues.send(self.request.event)
for receiver, response in responses:
if response:
issues.append(response)
return issues
def post(self, request, *args, **kwargs):

View File

@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView
from formtools.wizard.views import SessionWizardView
from pretix.base.models import Event, EventPermission
from pretix.base.models import Event, EventPermission, OrganizerPermission
from pretix.control.forms.event import (
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
)
@@ -48,6 +48,12 @@ class EventWizard(SessionWizardView):
'copy': condition_copy
}
def get_context_data(self, form, **kwargs):
ctx = super().get_context_data(form, **kwargs)
ctx['has_organizer'] = OrganizerPermission.objects.filter(user=self.request.user,
can_create_events=True).exists()
return ctx
def get_form_kwargs(self, step=None):
kwargs = {
'user': self.request.user

View File

@@ -1,12 +1,20 @@
from django import forms
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms import modelformset_factory
from django.shortcuts import redirect
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, UpdateView
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from pretix.base.models import Organizer, OrganizerPermission
from pretix.base.forms import I18nModelForm
from pretix.base.models import Organizer, OrganizerPermission, User
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import OrganizerForm, OrganizerUpdateForm
from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
class OrganizerList(ListView):
@@ -24,10 +32,147 @@ class OrganizerList(ListView):
)
class OrganizerPermissionForm(I18nModelForm):
class Meta:
model = OrganizerPermission
fields = (
'can_create_events', 'can_change_permissions'
)
class OrganizerPermissionCreateForm(OrganizerPermissionForm):
user = forms.EmailField(required=False, label=_('User'))
class OrganizerDetail(OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/detail.html'
permission = None
context_object_name = 'organizer'
def get_object(self, queryset=None) -> Organizer:
return self.request.organizer
@cached_property
def formset(self):
fs = modelformset_factory(
OrganizerPermission,
form=OrganizerPermissionForm,
can_delete=True, can_order=False, extra=0
)
return fs(data=self.request.POST if self.request.method == "POST" else None,
prefix="formset",
queryset=OrganizerPermission.objects.filter(organizer=self.request.organizer))
@cached_property
def add_form(self):
return OrganizerPermissionCreateForm(data=self.request.POST if self.request.method == "POST" else None,
prefix="add")
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
ctx['add_form'] = self.add_form
ctx['events'] = self.request.organizer.events.all()
return ctx
def _send_invite(self, instance):
try:
mail(
instance.invite_email,
_('Account information changed'),
'pretixcontrol/email/invitation_organizer.txt',
{
'user': self,
'organizer': self.request.organizer.name,
'url': build_absolute_uri('control:auth.invite', kwargs={
'token': instance.invite_token
})
},
event=None,
locale=self.request.LANGUAGE_CODE
)
except SendMailException:
pass # Already logged
@transaction.atomic
def post(self, *args, **kwargs):
if not self.request.orgaperm.can_change_permissions:
raise PermissionDenied(_("You have no permission to do this."))
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
logdata = {
k: v for k, v in self.add_form.cleaned_data.items()
}
try:
self.add_form.instance.organizer = self.request.organizer
self.add_form.instance.organizer_id = self.request.organizer.id
self.add_form.instance.user = User.objects.get(email=self.add_form.cleaned_data['user'])
self.add_form.instance.user_id = self.add_form.instance.user.id
except User.DoesNotExist:
self.add_form.instance.invite_email = self.add_form.cleaned_data['user']
if OrganizerPermission.objects.filter(invite_email=self.add_form.instance.invite_email,
organizer=self.request.organizer).exists():
messages.error(self.request, _('This user already has been invited for this team.'))
return self.get(*args, **kwargs)
self.add_form.save()
self._send_invite(self.add_form.instance)
self.request.organizer.log_action(
'pretix.organizer.permissions.invited', user=self.request.user, data=logdata
)
else:
if OrganizerPermission.objects.filter(user=self.add_form.instance.user,
organizer=self.request.organizer).exists():
messages.error(self.request, _('This user already has permissions for this team.'))
return self.get(*args, **kwargs)
self.add_form.save()
logdata['user'] = self.add_form.instance.user_id
self.request.organizer.log_action(
'pretix.organizer.permissions.added', user=self.request.user, data=logdata
)
for form in self.formset.forms:
if form.has_changed():
changedata = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
changedata['user'] = form.instance.user_id
self.request.organizer.log_action(
'pretix.organizer.permissions.changed', user=self.request.user, data=changedata
)
if form.instance.user_id == self.request.user.pk:
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms:
messages.error(self.request, _('You cannot remove your own permission to view this page.'))
return self.get(*args, **kwargs)
for form in self.formset.deleted_forms:
logdata = {
k: v for k, v in form.cleaned_data.items()
}
self.request.organizer.log_action(
'pretix.organizer.permissions.deleted', user=self.request.user, data=logdata
)
self.formset.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('Your changes could not be saved.'))
return self.get(*args, **kwargs)
def get_success_url(self) -> str:
return reverse('control:organizer', kwargs={
'organizer': self.request.organizer.slug,
})
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm
template_name = 'pretixcontrol/organizers/detail.html'
template_name = 'pretixcontrol/organizers/edit.html'
permission = None
context_object_name = 'organizer'

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-03 23:46+0000\n"
"POT-Creation-Date: 2017-01-07 12:08+0000\n"
"PO-Revision-Date: 2017-01-01 20:40+0100\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
@@ -46,33 +46,34 @@ msgstr "Gesamtumsatz"
msgid "Contacting Stripe …"
msgstr "Kontaktiere Stripe …"
#: static/pretixcontrol/js/clipboard.js:23
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr "Kopiert!"
#: static/pretixcontrol/js/clipboard.js:29
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr "Drücken Sie Strg+C zum Kopieren!"
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
#: pretix/static/pretixcontrol/js/ui/main.js:28
#: pretix/static/pretixpresale/js/ui/main.js:99
msgid "Close message"
msgstr "Schließen"
#: static/pretixcontrol/js/ui/main.js:43
#: pretix/static/pretixcontrol/js/ui/main.js:43
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: static/pretixcontrol/js/ui/question.js:41
#: pretix/static/pretixcontrol/js/ui/question.js:41
msgid "Others"
msgstr "Sonstige"
#: static/pretixcontrol/js/ui/question.js:70
#: pretix/static/pretixcontrol/js/ui/question.js:70
msgid "Count"
msgstr "Anzahl"
#: static/pretixpresale/js/ui/asyncdownload.js:27
#: static/pretixpresale/js/ui/asynctask.js:27
#: static/pretixpresale/js/ui/asynctask.js:64
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:64
msgid ""
"Your request has been queued on the server and will now be processed. If "
"this takes longer than two minutes, please contact us or go back in your "
@@ -82,19 +83,19 @@ msgstr ""
"dies länger als zwei Minuten dauert, kontaktieren Sie uns bitte oder gehen "
"Sie in Ihrem Browser einen Schritt zurück und versuchen es erneut."
#: static/pretixpresale/js/ui/asyncdownload.js:40
#: static/pretixpresale/js/ui/asynctask.js:43
#: static/pretixpresale/js/ui/asynctask.js:85
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:40
#: pretix/static/pretixpresale/js/ui/asynctask.js:43
#: pretix/static/pretixpresale/js/ui/asynctask.js:85
msgid "An error of type {code} occured."
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
#: static/pretixpresale/js/ui/asyncdownload.js:53
#: static/pretixpresale/js/ui/asynctask.js:103
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:53
#: pretix/static/pretixpresale/js/ui/asynctask.js:103
msgid "We are processing your request …"
msgstr "Wir verarbeiten deine Anfrage …"
#: static/pretixpresale/js/ui/asyncdownload.js:54
#: static/pretixpresale/js/ui/asynctask.js:104
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:54
#: pretix/static/pretixpresale/js/ui/asynctask.js:104
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -104,7 +105,7 @@ msgstr ""
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
"Seite neu laden und es erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:46
#: pretix/static/pretixpresale/js/ui/asynctask.js:46
msgid ""
"We currenctly cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -112,22 +113,22 @@ msgstr ""
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
"Letzter Fehlercode: {code}"
#: static/pretixpresale/js/ui/asynctask.js:76
#: pretix/static/pretixpresale/js/ui/asynctask.js:76
msgid "The request took to long. Please try again."
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:88
#: pretix/static/pretixpresale/js/ui/asynctask.js:88
msgid ""
"We currenctly cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuchen Sie es noch "
"einmal. Fehlercode: {code}"
#: static/pretixpresale/js/ui/cart.js:10
#: pretix/static/pretixpresale/js/ui/cart.js:10
msgid "The items in your cart are no longer reserved for you."
msgstr "Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert."
#: static/pretixpresale/js/ui/cart.js:14
#: pretix/static/pretixpresale/js/ui/cart.js:14
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-03 23:46+0000\n"
"POT-Creation-Date: 2017-01-07 12:08+0000\n"
"PO-Revision-Date: 2017-01-01 20:41+0100\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: \n"
@@ -46,33 +46,34 @@ msgstr "Gesamtumsatz"
msgid "Contacting Stripe …"
msgstr "Kontaktiere Stripe …"
#: static/pretixcontrol/js/clipboard.js:23
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr "Kopiert!"
#: static/pretixcontrol/js/clipboard.js:29
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr "Drücke Strg+C zum kopieren!"
#: static/pretixcontrol/js/ui/main.js:28 static/pretixpresale/js/ui/main.js:99
#: pretix/static/pretixcontrol/js/ui/main.js:28
#: pretix/static/pretixpresale/js/ui/main.js:99
msgid "Close message"
msgstr "Schließen"
#: static/pretixcontrol/js/ui/main.js:43
#: pretix/static/pretixcontrol/js/ui/main.js:43
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: static/pretixcontrol/js/ui/question.js:41
#: pretix/static/pretixcontrol/js/ui/question.js:41
msgid "Others"
msgstr "Sonstige"
#: static/pretixcontrol/js/ui/question.js:70
#: pretix/static/pretixcontrol/js/ui/question.js:70
msgid "Count"
msgstr "Anzahl"
#: static/pretixpresale/js/ui/asyncdownload.js:27
#: static/pretixpresale/js/ui/asynctask.js:27
#: static/pretixpresale/js/ui/asynctask.js:64
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:27
#: pretix/static/pretixpresale/js/ui/asynctask.js:64
msgid ""
"Your request has been queued on the server and will now be processed. If "
"this takes longer than two minutes, please contact us or go back in your "
@@ -82,19 +83,19 @@ msgstr ""
"dies länger als zwei Minuten dauert, kontaktiere uns bitte oder gehe in "
"deinem Browser einen Schritt zurück und versuche es erneut."
#: static/pretixpresale/js/ui/asyncdownload.js:40
#: static/pretixpresale/js/ui/asynctask.js:43
#: static/pretixpresale/js/ui/asynctask.js:85
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:40
#: pretix/static/pretixpresale/js/ui/asynctask.js:43
#: pretix/static/pretixpresale/js/ui/asynctask.js:85
msgid "An error of type {code} occured."
msgstr "Ein Fehler ist aufgetreten. Fehlercode: {code}"
#: static/pretixpresale/js/ui/asyncdownload.js:53
#: static/pretixpresale/js/ui/asynctask.js:103
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:53
#: pretix/static/pretixpresale/js/ui/asynctask.js:103
msgid "We are processing your request …"
msgstr "Wir verarbeiten deine Anfrage …"
#: static/pretixpresale/js/ui/asyncdownload.js:54
#: static/pretixpresale/js/ui/asynctask.js:104
#: pretix/static/pretixpresale/js/ui/asyncdownload.js:54
#: pretix/static/pretixpresale/js/ui/asynctask.js:104
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
@@ -104,7 +105,7 @@ msgstr ""
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
"neu laden und es erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:46
#: pretix/static/pretixpresale/js/ui/asynctask.js:46
msgid ""
"We currenctly cannot reach the server, but we keep trying. Last error code: "
"{code}"
@@ -112,22 +113,22 @@ msgstr ""
"Wir können den Server aktuell nicht erreichen, versuchen es aber weiter. "
"Letzter Fehlercode: {code}"
#: static/pretixpresale/js/ui/asynctask.js:76
#: pretix/static/pretixpresale/js/ui/asynctask.js:76
msgid "The request took to long. Please try again."
msgstr "Diese Anfrage hat zu lange gedauert. Bitte erneut versuchen."
#: static/pretixpresale/js/ui/asynctask.js:88
#: pretix/static/pretixpresale/js/ui/asynctask.js:88
msgid ""
"We currenctly cannot reach the server. Please try again. Error code: {code}"
msgstr ""
"Wir können den Server aktuell nicht erreichen. Bitte versuche es noch "
"einmal. Fehlercode: {code}"
#: static/pretixpresale/js/ui/cart.js:10
#: pretix/static/pretixpresale/js/ui/cart.js:10
msgid "The items in your cart are no longer reserved for you."
msgstr "Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert."
#: static/pretixpresale/js/ui/cart.js:14
#: pretix/static/pretixpresale/js/ui/cart.js:14
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""

View File

@@ -51,6 +51,7 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[0-9]+)$',
pretix.presale.views.order.InvoiceDownload.as_view(),
name='event.invoice.download'),
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'),
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
]

View File

@@ -1,5 +1,7 @@
from importlib import import_module
from urllib.parse import urljoin
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import resolve
from django.http import Http404
@@ -11,6 +13,8 @@ from pretix.base.models import Event, EventPermission, Organizer
from pretix.multidomain.urlreverse import get_domain
from pretix.presale.signals import process_request, process_response
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
def _detect_event(request, require_live=True):
url = resolve(request.path_info)
@@ -59,8 +63,24 @@ def _detect_event(request, require_live=True):
LocaleMiddleware().process_request(request)
if require_live and not request.event.live:
if not request.user.is_authenticated or not EventPermission.objects.filter(
event=request.event, user=request.user).exists():
can_access = (
url.url_name == 'event.auth'
or (
request.user.is_authenticated
and EventPermission.objects.filter(event=request.event, user=request.user).exists()
)
)
if not can_access and 'pretix_event_access_{}'.format(request.event.pk) in request.session:
sparent = SessionStore(request.session.get('pretix_event_access_{}'.format(request.event.pk)))
try:
parentdata = sparent.load()
except:
pass
else:
can_access = 'event_access' in parentdata
if not can_access:
raise PermissionDenied(_('The selected ticket shop is currently not available.'))
for receiver, response in process_request.send(request.event, request=request):

View File

@@ -1,13 +1,24 @@
import sys
from importlib import import_module
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Prefetch, Q
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from pretix.base.models import ItemVariation
from pretix.multidomain.urlreverse import eventreverse
from . import CartMixin, EventViewMixin
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
def item_group_by_category(items):
return sorted(
@@ -90,3 +101,32 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
context['cart'] = self.get_cart()
context['frontpage_text'] = str(self.request.event.settings.frontpage_text)
return context
class EventAuth(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
s = SessionStore(request.POST.get('session'))
try:
data = s.load()
except:
raise PermissionDenied(_('Please go back and try again.'))
parent = data.get('pretix_event_access_{}'.format(request.event.pk))
sparent = SessionStore(parent)
try:
parentdata = sparent.load()
except:
raise PermissionDenied(_('Please go back and try again.'))
else:
if 'event_access' not in parentdata:
raise PermissionDenied(_('Please go back and try again.'))
request.session['pretix_event_access_{}'.format(request.event.pk)] = parent
return redirect(eventreverse(request.event, 'presale:event.index'))

View File

@@ -2,7 +2,7 @@
Django==1.10.*
python-dateutil
pytz
django-bootstrap3==7.1.*
django-bootstrap3==8.0.*
django-formset-js-improved==0.5.0.1
django-compressor==2.1
reportlab==3.2.*

View File

@@ -23,22 +23,12 @@ from pretix.base.services.orders import (
class UserTestCase(TestCase):
def test_name(self):
u = User.objects.create_user('test@foo.bar', 'test')
u.givenname = "Christopher"
u.familyname = "Nolan"
u.fullname = "Christopher Nolan"
u.set_password("test")
u.save()
self.assertEqual(u.get_full_name(), 'Nolan, Christopher')
self.assertEqual(u.get_short_name(), 'Christopher')
u.givenname = None
u.save()
self.assertEqual(u.get_full_name(), 'Nolan')
self.assertEqual(u.get_short_name(), 'Nolan')
u.givenname = "Christopher"
u.familyname = None
u.save()
self.assertEqual(u.get_full_name(), 'Christopher')
self.assertEqual(u.get_short_name(), 'Christopher')
u.givenname = None
self.assertEqual(u.get_full_name(), 'Christopher Nolan')
self.assertEqual(u.get_short_name(), 'Christopher Nolan')
u.fullname = None
u.save()
self.assertEqual(u.get_full_name(), 'test@foo.bar')
self.assertEqual(u.get_short_name(), 'test@foo.bar')

View File

@@ -72,6 +72,7 @@ event_urls = [
organizer_urls = [
'organizer/abc/edit',
'organizer/abc/',
]

View File

@@ -26,13 +26,11 @@ class UserSettingsTest(SoupTest):
def test_set_name(self):
doc = self.save({
'givenname': 'Peter',
'familyname': 'Miller'
'fullname': 'Peter Miller',
})
assert doc.select(".alert-success")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.givenname == 'Peter'
assert self.user.familyname == 'Miller'
assert self.user.fullname == 'Peter Miller'
def test_change_email_require_password(self):
doc = self.save({