diff --git a/src/pretix/base/forms/__init__.py b/src/pretix/base/forms/__init__.py
index 339b3fe379..48cf00365e 100644
--- a/src/pretix/base/forms/__init__.py
+++ b/src/pretix/base/forms/__init__.py
@@ -50,7 +50,7 @@ class SettingsForm(forms.Form):
def __init__(self, *args, **kwargs):
self.obj = kwargs.pop('obj')
- kwargs['initial'] = self.obj.settings
+ kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs)
def save(self):
diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py
index f4d82e71f9..69df674e5a 100644
--- a/src/pretix/base/i18n.py
+++ b/src/pretix/base/i18n.py
@@ -3,7 +3,8 @@ import json
from django import forms
from django.conf import settings
-from django.db.models import SubfieldBase, TextField
+from django.core.serializers.json import DjangoJSONEncoder
+from django.db.models import Model, QuerySet, SubfieldBase, TextField
from django.utils import translation
from django.utils.safestring import mark_safe
from typing import Dict, List
@@ -233,3 +234,15 @@ class I18nTextField(I18nFieldMixin, TextField, metaclass=SubfieldBase):
Like I18nCharField, but for TextFields.
"""
widget = I18nTextarea
+
+
+class I18nJSONEncoder(DjangoJSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, LazyI18nString):
+ return obj.data
+ elif isinstance(obj, QuerySet):
+ return list(obj)
+ elif isinstance(obj, Model):
+ return {'type': obj.__class__.__name__, 'id': obj.id}
+ else:
+ return super().default(obj)
diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py
index aef87c5d66..f58260a565 100644
--- a/src/pretix/base/models/auth.py
+++ b/src/pretix/base/models/auth.py
@@ -5,6 +5,8 @@ from django.contrib.auth.models import (
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from .base import LoggingMixin
+
class UserManager(BaseUserManager):
"""
@@ -30,7 +32,7 @@ class UserManager(BaseUserManager):
return user
-class User(AbstractBaseUser, PermissionsMixin):
+class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
This is the user model used by pretix for authentication.
diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py
index cea57f989a..658ca12b6d 100644
--- a/src/pretix/base/models/base.py
+++ b/src/pretix/base/models/base.py
@@ -1,3 +1,4 @@
+import json
import uuid
from django.contrib.contenttypes.fields import GenericRelation
@@ -5,6 +6,8 @@ from django.db import models
from django.db.models.signals import post_delete
from django.dispatch import receiver
+from pretix.base.i18n import I18nJSONEncoder
+
def cachedfile_name(instance, filename: str) -> str:
return 'cachedfiles/%012d.%s' % (instance.id, filename.split('.')[-1])
@@ -29,10 +32,18 @@ def cached_file_delete(sender, instance, **kwargs):
instance.file.delete(False)
-class LoggedModel(models.Model):
+class LoggingMixin:
logentries = GenericRelation('LogEntry')
- def log_action(self, user, action, data):
+ def log_action(self, action, data=None, user=None):
+ """
+ Create a LogEntry object that is related to this object.
+ See the LogEntry documentation for details.
+
+ :param action: The namespaced action code
+ :param data: Any JSON-serializable object
+ :param user: The user performing the action (optional)
+ """
from .log import LogEntry
from .event import Event
@@ -41,7 +52,13 @@ class LoggedModel(models.Model):
event = self
elif hasattr(self, 'event'):
event = self.event
- LogEntry.objects.create(content_object=self, user=user, action=action, data=data, event=event)
+ l = LogEntry(content_object=self, user=user, action_type=action, event=event)
+ if data:
+ l.data = json.dumps(data, cls=I18nJSONEncoder)
+ l.save()
+
+
+class LoggedModel(models.Model, LoggingMixin):
class Meta:
abstract = True
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index 18f58bc62b..b42dcd1862 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -2,7 +2,7 @@ import random
import string
from datetime import datetime
-from django.db import models
+from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from typing import List, Union
@@ -177,14 +177,6 @@ class Order(LoggedModel):
return True
return False # nothing there to modify
- def mark_refunded(self):
- """
- Mark this order as refunded. This sets the payment status and returns the order object.
- """
- self.status = Order.STATUS_REFUNDED
- self.save()
- return self
-
def _can_be_paid(self) -> Union[bool, str]:
error_messages = {
'late': _("The payment is too late to be accepted."),
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index d94d6eee71..6bc2e00561 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -359,6 +359,7 @@ class BasePaymentProvider:
:param order: The order object
"""
+ self.order.log_action('pretix.base.order.refunded')
return '
%s
' % _('The money can not be automatically refunded, '
'please transfer the money back manually.')
@@ -379,7 +380,9 @@ class BasePaymentProvider:
:param request: The HTTP request
:param order: The order object
"""
- order.mark_refunded()
+ from pretix.base.services.orders import mark_order_refunded
+
+ mark_order_refunded(order, user=request.user)
messages.success(request, _('The order has been marked as refunded. Please transfer the money '
'back to the buyer manually.'))
@@ -440,7 +443,9 @@ class FreeOrderProvider(BasePaymentProvider):
:param request: The HTTP request
:param order: The order object
"""
- order.mark_refunded()
+ from pretix.base.services.orders import mark_order_refunded
+
+ mark_order_refunded(order, user=request.user)
messages.success(request, _('The order has been marked as refunded.'))
def is_allowed(self, request: HttpRequest) -> bool:
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 12abd79cde..33b6ebba9b 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from typing import List
from pretix.base.models import (
- CartPosition, Event, EventLock, Order, OrderPosition, Quota,
+ CartPosition, Event, EventLock, Order, OrderPosition, Quota, User,
)
from pretix.base.payment import BasePaymentProvider
from pretix.base.services.mail import mail
@@ -31,7 +31,7 @@ error_messages = {
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
- force: bool=False) -> Order:
+ force: bool=False, user: User=None) -> Order:
"""
Marks an order as paid. This sets the payment provider, info and date and returns
the order object.
@@ -46,6 +46,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
:param force: Whether this payment should be marked as paid even if no remaining
quota is available (default: ``False``).
:type force: boolean
+ :param user: The user that performed the change
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
"""
with order.event.lock():
@@ -59,6 +60,13 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
order.payment_manual = manual
order.status = Order.STATUS_PAID
order.save()
+ order.log_action('pretix.event.order.paid', {
+ 'provider': provider,
+ 'info': info,
+ 'date': date,
+ 'manual': manual,
+ 'force': force
+ }, user=user)
order_paid.send(order.event, order=order)
mail(
@@ -78,6 +86,32 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
return order
+@transaction.atomic
+def mark_order_refunded(order: Order, user: User=None):
+ """
+ Mark this order as refunded. This sets the payment status and returns the order object.
+ :param order: The order to change
+ :param user: The user that performed the change
+ """
+ order.status = Order.STATUS_REFUNDED
+ order.save()
+ order.log_action('pretix.event.order.refunded', user=user)
+ return order
+
+
+@transaction.atomic
+def cancel_order(order: Order, user: User=None):
+ """
+ Mark this order as canceled
+ :param order: The order to change
+ :param user: The user that performed the change
+ """
+ order.status = Order.STATUS_CANCELLED
+ order.save()
+ order.log_action('pretix.event.order.cancelled', user=user)
+ return order
+
+
class OrderError(Exception):
pass
@@ -154,6 +188,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], dt: d
payment_provider=payment_provider.identifier
)
OrderPosition.transform_cart_positions(positions, order)
+ order.log_action('pretix.event.order.placed')
order_placed.send(event, order=order)
return order
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index 83f4299166..63d7722ae5 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -121,7 +121,20 @@ class SettingsProxy:
def _flush(self) -> None:
self._cached_obj = None
+ def freeze(self):
+ settings = {}
+ for key, v in DEFAULTS.items():
+ settings[key] = self._unserialize(v['default'], v['type'])
+ if self._parent:
+ settings.update(self._parent.settings.freeze())
+ for key, value in self._cache().items():
+ settings[key] = self.get(key)
+ return settings
+
def _unserialize(self, value: str, as_type: type) -> Any:
+ if as_type is None and value is not None and value.startswith('file://'):
+ as_type = File
+
if as_type is not None and isinstance(value, as_type):
return value
elif value is None:
@@ -186,15 +199,14 @@ class SettingsProxy:
if value is None and default is not None:
value = default
- if as_type is None and value is not None and value.startswith('file://'):
- as_type = File
-
return self._unserialize(value, as_type)
def __getitem__(self, key: str) -> Any:
return self.get(key)
def __getattr__(self, key: str) -> Any:
+ if key.startswith('_'):
+ return super().__getattr__(key)
return self.get(key)
def __setattr__(self, key: str, value: Any) -> None:
diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py
index be74cc989a..27b67e3ccc 100644
--- a/src/pretix/control/views/auth.py
+++ b/src/pretix/control/views/auth.py
@@ -62,6 +62,7 @@ def register(request):
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)
return redirect('control:index')
else:
@@ -90,6 +91,7 @@ class Forgot(TemplateView):
},
None, locale=user.locale
)
+ user.log_action('pretix.control.auth.user.forgot_password.mail_sent')
messages.success(request, _('We sent you an e-mail containing further instructions.'))
return redirect('control:auth.forgot')
else:
@@ -141,6 +143,7 @@ class Recover(TemplateView):
user.set_password(self.form.cleaned_data['password'])
user.save()
messages.success(request, _('You can now login using your new password.'))
+ user.log_action('pretix.control.auth.user.forgot_password.recovered')
return redirect('control:auth.login')
else:
return self.get(request, *args, **kwargs)
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index bce61f3fe0..c5f82039f8 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -3,6 +3,7 @@ from collections import OrderedDict
from django import forms
from django.contrib import messages
from django.core.urlresolvers import reverse
+from django.db import transaction
from django.db.models import Sum
from django.forms import modelformset_factory
from django.shortcuts import redirect, render
@@ -53,8 +54,17 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
context['sform'] = self.sform
return context
+ @transaction.atomic()
def form_valid(self, form):
self.sform.save()
+ if self.sform.has_changed():
+ self.request.event.log_action('pretix.event.settings', user=self.request.user, data={
+ k: self.request.event.settings.get(k) for k in self.sform.changed_data
+ })
+ if form.has_changed():
+ self.request.event.log_action('pretix.event.changed', user=self.request.user, data={
+ k: getattr(self.request.event, k) for k in form.changed_data
+ })
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
@@ -97,15 +107,20 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
def post(self, request, *args, **kwargs):
self.object = self.get_object()
plugins_active = self.object.get_plugins()
- for key, value in request.POST.items():
- if key.startswith("plugin:"):
- module = key.split(":")[1]
- if value == "enable":
- plugins_active.append(module)
- else:
- plugins_active.remove(module)
- self.object.plugins = ",".join(plugins_active)
- self.object.save()
+ with transaction.atomic():
+ for key, value in request.POST.items():
+ if key.startswith("plugin:"):
+ module = key.split(":")[1]
+ if value == "enable":
+ self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
+ data={'plugin': module})
+ plugins_active.append(module)
+ else:
+ self.request.event.log_action('pretix.event.plugins.disabled', user=self.request.user,
+ data={'plugin': module})
+ plugins_active.remove(module)
+ self.object.plugins = ",".join(plugins_active)
+ self.object.save()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
@@ -159,11 +174,18 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
context['providers'] = self.provider_forms
return self.render_to_response(context)
+ @transaction.atomic()
def post(self, request, *args, **kwargs):
self.object = self.get_object()
success = True
for provider in self.provider_forms:
if provider.form.is_valid():
+ if provider.form.has_changed():
+ self.request.event.log_action(
+ 'pretix.event.payment.provider.' + provider.identifier, user=self.request.user, data={
+ k: provider.form.cleaned_data.get(k) for k in provider.form.changed_data
+ }
+ )
provider.form.save()
else:
success = False
@@ -207,16 +229,29 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
form.prepare_fields()
return form
+ @transaction.atomic()
def post(self, request, *args, **kwargs):
success = True
for provider in self.provider_forms:
if provider.form.is_valid():
provider.form.save()
+ if provider.form.has_changed():
+ self.request.event.log_action(
+ 'pretix.event.tickets.provider.' + provider.identifier, user=self.request.user, data={
+ k: provider.form.cleaned_data.get(k) for k in provider.form.changed_data
+ }
+ )
else:
success = False
form = self.get_form(self.get_form_class())
if success and form.is_valid():
form.save()
+ if form.has_changed():
+ self.request.event.log_action(
+ 'pretix.event.tickets.settings', user=self.request.user, data={
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ )
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
@@ -310,6 +345,7 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
ctx['add_form'] = self.add_form
return ctx
+ @transaction.atomic()
def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed():
@@ -327,7 +363,22 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
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
+ )
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.event.log_action(
+ 'pretix.event.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.'))
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py
index a83f8dbb52..b2eb09c144 100644
--- a/src/pretix/control/views/item.py
+++ b/src/pretix/control/views/item.py
@@ -103,12 +103,14 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
except ItemCategory.DoesNotExist:
raise Http404(_("The requested product category does not exist."))
+ @transaction.atomic()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
for item in self.object.items.all():
item.category = None
item.save()
success_url = self.get_success_url()
+ self.object.log_action('pretix.event.category.deleted', user=self.request.user)
self.object.delete()
messages.success(request, _('The selected category has been deleted.'))
return HttpResponseRedirect(success_url)
@@ -136,8 +138,15 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
except ItemCategory.DoesNotExist:
raise Http404(_("The requested product category does not exist."))
+ @transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
+ if form.has_changed():
+ self.object.log_action(
+ 'pretix.event.category.changed', user=self.request.user, data={
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ )
return super().form_valid(form)
def get_success_url(self) -> str:
@@ -160,10 +169,13 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
'event': self.request.event.slug,
})
+ @transaction.atomic()
def form_valid(self, form):
form.instance.event = self.request.event
messages.success(self.request, _('The new category has been created.'))
- return super().form_valid(form)
+ ret = super().form_valid(form)
+ form.instance.log_action('pretix.event.category.added', data=dict(form.cleaned_data), user=self.request.user)
+ return ret
class CategoryList(ListView):
@@ -248,9 +260,11 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
context['dependent'] = list(self.get_object().items.all())
return context
+ @transaction.atomic()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
+ self.object.log_action(action='pretix.event.question.deleted', user=request.user)
self.object.delete()
messages.success(request, _('The selected question has been deleted.'))
return HttpResponseRedirect(success_url)
@@ -277,7 +291,14 @@ class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
except Question.DoesNotExist:
raise Http404(_("The requested question does not exist."))
+ @transaction.atomic()
def form_valid(self, form):
+ if form.has_changed():
+ self.object.log_action(
+ 'pretix.event.question.changed', user=self.request.user, data={
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ )
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
@@ -306,9 +327,12 @@ class QuestionCreate(EventPermissionRequiredMixin, CreateView):
'event': self.request.event.slug,
})
+ @transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _('The new question has been created.'))
- return super().form_valid(form)
+ ret = super().form_valid(form)
+ form.instance.log_action('pretix.event.question.added', user=self.request.user, data=dict(form.cleaned_data))
+ return ret
class QuotaList(ListView):
@@ -378,10 +402,13 @@ class QuotaCreate(EventPermissionRequiredMixin, QuotaEditorMixin, CreateView):
'event': self.request.event.slug,
})
+ @transaction.atomic()
def form_valid(self, form):
form.instance.event = self.request.event
messages.success(self.request, _('The new quota has been created.'))
- return super().form_valid(form)
+ ret = super().form_valid(form)
+ form.instance.log_action('pretix.event.quota.added', user=self.request.user, data=dict(form.cleaned_data))
+ return ret
class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
@@ -399,8 +426,15 @@ class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
except Quota.DoesNotExist:
raise Http404(_("The requested quota does not exist."))
+ @transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
+ if form.has_changed():
+ self.object.log_action(
+ 'pretix.event.quota.changed', user=self.request.user, data={
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ )
return super().form_valid(form)
def get_success_url(self) -> str:
@@ -429,9 +463,11 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
context['dependent'] = list(self.get_object().items.all())
return context
+ @transaction.atomic()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
+ self.object.log_action(action='pretix.event.quota.deleted', user=request.user)
self.object.delete()
messages.success(self.request, _('The selected quota has been deleted.'))
return HttpResponseRedirect(success_url)
@@ -471,9 +507,12 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
'item': self.object.id,
})
+ @transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
- return super().form_valid(form)
+ ret = super().form_valid(form)
+ form.instance.log_action('pretix.event.item.added', user=self.request.user, data=dict(form.cleaned_data))
+ return ret
def get_form_kwargs(self):
"""
@@ -497,8 +536,15 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
'item': self.get_object().id,
})
+ @transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
+ if form.has_changed():
+ self.object.log_action(
+ 'pretix.event.item.changed', user=self.request.user, data={
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ )
return super().form_valid(form)
@@ -543,13 +589,31 @@ class ItemProperties(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
for f in formset:
f.instance.event = self.request.event
f.instance.item = self.get_object()
+ is_created = not f.instance.pk
f.instance.save()
- print(f.instance)
+ if f.has_changed() and not is_created:
+ change_data = {
+ k: f.cleaned_data.get(k) for k in f.changed_data
+ }
+ change_data['id'] = f.instance.pk
+ f.instance.item.log_action(
+ 'pretix.event.item.property.changed', user=self.request.user, data=change_data
+ )
+ elif is_created:
+ change_data = dict(f.cleaned_data)
+ change_data['id'] = f.instance.pk
+ f.instance.item.log_action(
+ 'pretix.event.item.property.added', user=self.request.user, data=change_data
+ )
for n in f.nested:
- print(n.deleted_forms, n.ordered_forms, n.extra_forms)
for fn in n.deleted_forms:
+ f.instance.item.log_action(
+ 'pretix.event.item.property.value.deleted', user=self.request.user, data={
+ 'id': fn.instance.pk
+ }
+ )
fn.instance.delete()
fn.instance.pk = None
@@ -557,8 +621,24 @@ class ItemProperties(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
fn.instance.position = i
fn.instance.prop = f.instance
fn.save()
+ if f.has_changed():
+ change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
+ change_data['id'] = f.instance.pk
+ f.instance.item.log_action(
+ 'pretix.event.item.property.value.changed', user=self.request.user, data=change_data
+ )
- n.save_new_objects()
+ for form in n.extra_forms:
+ if not form.has_changed():
+ continue
+ if n.can_delete and n._should_delete_form(form):
+ continue
+ change_data = dict(f.cleaned_data)
+ n.save_new(form)
+ change_data['id'] = form.instance.pk
+ f.instance.item.log_action(
+ 'pretix.event.item.property.value.added', user=self.request.user, data=change_data
+ )
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
@@ -700,6 +780,13 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
for form in self.forms_flat:
if form.is_valid() and form.has_changed():
form.save()
+ change_data = {
+ k: form.cleaned_data.get(k) for k in form.changed_data
+ }
+ change_data['id'] = form.instance.pk
+ self.object.log_action(
+ 'pretix.event.item.variation.changed', user=self.request.user, data=change_data
+ )
if hasattr(form.instance, 'creation') and form.instance.creation:
# We need this special 'creation' field set to true in get_form
# for newly created items as cleanerversion does already set the
@@ -758,9 +845,11 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
raise Http404(_("The requested product does not exist."))
return self.object
+ @transaction.atomic
def delete(self, request, *args, **kwargs):
success_url = self.get_success_url()
if self.is_allowed():
+ self.get_object().log_action('pretix.event.item.deleted', user=self.request.user)
self.get_object().delete()
messages.success(request, _('The selected product has been deleted.'))
return HttpResponseRedirect(success_url)
@@ -768,6 +857,9 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
o = self.get_object()
o.active = False
o.save()
+ o.log_action('pretix.event.item.changed', user=self.request.user, data={
+ 'active': False
+ })
messages.success(request, _('The selected product has been deactivated.'))
return HttpResponseRedirect(success_url)
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py
index d108422906..9d782cc146 100644
--- a/src/pretix/control/views/orders.py
+++ b/src/pretix/control/views/orders.py
@@ -17,7 +17,7 @@ from pretix.base.models import (
)
from pretix.base.services import tickets
from pretix.base.services.export import export
-from pretix.base.services.orders import mark_order_paid
+from pretix.base.services.orders import cancel_order, mark_order_paid
from pretix.base.services.stats import order_overview
from pretix.base.signals import (
register_data_exporters, register_payment_providers,
@@ -136,8 +136,8 @@ class OrderDetail(OrderView):
def keyfunc(pos):
if (pos.item.admission and self.request.event.settings.attendee_names_asked) \
or pos.item.questions.all():
- return pos.id, "", "", ""
- return "", pos.item_id, pos.variation_id, pos.price
+ return pos.id, 0, 0, 0
+ return 0, pos.item_id, pos.variation_id, pos.price
positions = []
for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc):
@@ -164,19 +164,19 @@ class OrderTransition(OrderView):
to = self.request.POST.get('status', '')
if self.order.status == 'n' and to == 'p':
try:
- mark_order_paid(self.order, manual=True)
+ mark_order_paid(self.order, manual=True, user=self.request.user)
except Quota.QuotaExceededException as e:
messages.error(self.request, str(e))
else:
messages.success(self.request, _('The order has been marked as paid.'))
elif self.order.status == 'n' and to == 'c':
- self.order.status = Order.STATUS_CANCELLED
- self.order.save()
+ cancel_order(self.order, user=self.request.user)
messages.success(self.request, _('The order has been cancelled.'))
elif self.order.status == 'p' and to == 'n':
self.order.status = Order.STATUS_PENDING
self.order.payment_manual = True
self.order.save()
+ self.order.log_action('pretix.base.order.unpaid', user=self.request.user)
messages.success(self.request, _('The order has been marked as not paid.'))
elif self.order.status == 'p' and to == 'r':
ret = self.payment_provider.order_control_refund_perform(self.request, self.order)
@@ -247,6 +247,9 @@ class OrderExtend(OrderView):
if self.form.is_valid():
if oldvalue > now():
messages.success(self.request, _('The payment term has been changed.'))
+ self.order.log_action('pretix.order.changed', user=self.request.user, data={
+ 'expires': self.order.expires
+ })
self.form.save()
else:
try:
@@ -254,6 +257,9 @@ class OrderExtend(OrderView):
is_available = self.order._is_still_available()
if is_available is True:
self.form.save()
+ self.order.log_action('pretix.order.changed', user=self.request.user, data={
+ 'expires': self.order.expires
+ })
messages.success(self.request, _('The payment term has been changed.'))
else:
messages.error(self.request, is_available)
diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py
index e466188125..805e965e91 100644
--- a/src/pretix/plugins/paypal/payment.py
+++ b/src/pretix/plugins/paypal/payment.py
@@ -10,7 +10,7 @@ from django.utils.translation import ugettext as __, ugettext_lazy as _
from pretix.base.models import Quota
from pretix.base.payment import BasePaymentProvider
-from pretix.base.services.orders import mark_order_paid
+from pretix.base.services.orders import mark_order_paid, mark_order_refunded
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger('pretix.plugins.paypal')
@@ -218,7 +218,7 @@ class Paypal(BasePaymentProvider):
payment_info = None
if not payment_info:
- order.mark_refunded()
+ mark_order_refunded(order)
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
return
@@ -231,11 +231,11 @@ class Paypal(BasePaymentProvider):
refund = sale.refund({})
if not refund.success():
- order.mark_refunded()
+ mark_order_refunded(order, user=request.user)
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
else:
sale = paypalrestsdk.Payment.find(payment_info['id'])
- order = order.mark_refunded()
+ order = mark_order_refunded(order)
order.payment_info = json.dumps(sale.to_dict())
order.save()
diff --git a/src/pretix/plugins/sendmail/views.py b/src/pretix/plugins/sendmail/views.py
index a33a94ad08..72e5ec282f 100644
--- a/src/pretix/plugins/sendmail/views.py
+++ b/src/pretix/plugins/sendmail/views.py
@@ -27,12 +27,15 @@ class SenderView(EventPermissionRequiredMixin, FormView):
def form_valid(self, form):
orders = Order.objects.filter(
event=self.request.event, status__in=form.cleaned_data['sendto']
- ).select_related("user")
- users = set([o.user for o in orders])
+ )
+ mails = set([o.email for o in orders])
- for u in users:
- mail(u.email, form.cleaned_data['subject'], form.cleaned_data['message'],
- None, self.request.event, locale=u.locale)
+ self.request.event.log_action('pretix.plugins.sendmail.sent', user=self.request.user, data=dict(
+ form.cleaned_data))
+
+ for m in mails:
+ mail(m, form.cleaned_data['subject'], form.cleaned_data['message'],
+ None, self.request.event, locale=m.locale)
messages.success(self.request, _('Your message will be sent to the selected users.'))
diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py
index 6fbe062c46..e16869f0d8 100644
--- a/src/pretix/plugins/stripe/payment.py
+++ b/src/pretix/plugins/stripe/payment.py
@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Quota
from pretix.base.payment import BasePaymentProvider
-from pretix.base.services.orders import mark_order_paid
+from pretix.base.services.orders import mark_order_paid, mark_order_refunded
from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger('pretix.plugins.stripe')
@@ -157,7 +157,7 @@ class Stripe(BasePaymentProvider):
payment_info = None
if not payment_info:
- order.mark_refunded()
+ mark_order_refunded(order, user=request.user)
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
return
@@ -173,10 +173,10 @@ class Stripe(BasePaymentProvider):
'support if the problem persists.'))
logger.error('Stripe error: %s' % str(err))
except stripe.error.StripeError:
- order.mark_refunded()
+ mark_order_refunded(order)
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
else:
- order = order.mark_refunded()
+ order = mark_order_refunded(order)
order.payment_info = str(ch)
order.save()
diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py
index 42bfc200c9..bcaf6a9e06 100644
--- a/src/pretix/plugins/stripe/views.py
+++ b/src/pretix/plugins/stripe/views.py
@@ -7,6 +7,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from pretix.base.models import Event, Order
+from pretix.base.services.orders import mark_order_refunded
from pretix.plugins.stripe.payment import Stripe
logger = logging.getLogger('pretix.plugins.stripe')
@@ -42,6 +43,8 @@ def webhook(request):
prov = Stripe(event)
prov._init_api()
+ order.log_action('pretix.plugins.stripe.event', data=event_json)
+
try:
charge = stripe.Charge.retrieve(charge['id'])
except stripe.error.StripeError as err:
@@ -49,6 +52,6 @@ def webhook(request):
return HttpResponse('StripeError', status=500)
if charge['refunds']['total_count'] > 0 and order.status == Order.STATUS_PAID:
- order.mark_refunded()
+ mark_order_refunded(order)
return HttpResponse(status=200)
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index ed8b796ea3..d0aad2b795 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View
from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition
+from pretix.base.services.orders import cancel_order
from pretix.base.services.tickets import generate
from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
@@ -215,6 +216,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
messages.error(self.request,
_("We had difficulties processing your input. Please review the errors below."))
return self.get(request, *args, **kwargs)
+ self.order.log_action('pretix.event.order.modified')
return redirect(self.get_order_url())
def get(self, request, *args, **kwargs):
@@ -251,8 +253,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
- self.order.status = Order.STATUS_CANCELLED
- self.order.save()
+ cancel_order(self.order)
return redirect(self.get_order_url())
def get(self, request, *args, **kwargs):
diff --git a/src/tests/base/test_settings.py b/src/tests/base/test_settings.py
index 8249559b2e..6f7defeeda 100644
--- a/src/tests/base/test_settings.py
+++ b/src/tests/base/test_settings.py
@@ -12,7 +12,6 @@ from pretix.base.settings import SettingsSandbox
class SettingsTestCase(TestCase):
-
def setUp(self):
settings.DEFAULTS['test_default'] = {
'default': 'def',
@@ -149,6 +148,7 @@ class SettingsTestCase(TestCase):
def test_serialize_unknown(self):
class Type:
pass
+
try:
self._test_serialization(Type(), Type)
self.assertTrue(False, 'No exception thrown!')
@@ -195,3 +195,23 @@ class SettingsTestCase(TestCase):
self.assertIsNone(sandbox.bar)
self.assertIsNone(sandbox['baz'])
+
+ def test_freeze(self):
+ olddef = settings.DEFAULTS
+ settings.DEFAULTS = {
+ 'test_default': {
+ 'default': 'def',
+ 'type': str
+ }
+ }
+ self.event.organizer.settings.set('bar', 'baz')
+ self.event.organizer.settings.set('foo', 'baz')
+ self.event.settings.set('foo', 'bar')
+ try:
+ self.assertEqual(self.event.settings.freeze(), {
+ 'test_default': 'def',
+ 'bar': 'baz',
+ 'foo': 'bar'
+ })
+ finally:
+ settings.DEFAULTS = olddef