Added logging for all basic operations

This commit is contained in:
Raphael Michel
2015-12-12 22:35:25 +01:00
parent 83b5fa2fa6
commit 58b85819bc
18 changed files with 316 additions and 61 deletions

View File

@@ -50,7 +50,7 @@ class SettingsForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.obj = kwargs.pop('obj') self.obj = kwargs.pop('obj')
kwargs['initial'] = self.obj.settings kwargs['initial'] = self.obj.settings.freeze()
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -3,7 +3,8 @@ import json
from django import forms from django import forms
from django.conf import settings 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 import translation
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from typing import Dict, List from typing import Dict, List
@@ -233,3 +234,15 @@ class I18nTextField(I18nFieldMixin, TextField, metaclass=SubfieldBase):
Like I18nCharField, but for TextFields. Like I18nCharField, but for TextFields.
""" """
widget = I18nTextarea 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)

View File

@@ -5,6 +5,8 @@ from django.contrib.auth.models import (
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .base import LoggingMixin
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
""" """
@@ -30,7 +32,7 @@ class UserManager(BaseUserManager):
return user return user
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
""" """
This is the user model used by pretix for authentication. This is the user model used by pretix for authentication.

View File

@@ -1,3 +1,4 @@
import json
import uuid import uuid
from django.contrib.contenttypes.fields import GenericRelation 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.db.models.signals import post_delete
from django.dispatch import receiver from django.dispatch import receiver
from pretix.base.i18n import I18nJSONEncoder
def cachedfile_name(instance, filename: str) -> str: def cachedfile_name(instance, filename: str) -> str:
return 'cachedfiles/%012d.%s' % (instance.id, filename.split('.')[-1]) return 'cachedfiles/%012d.%s' % (instance.id, filename.split('.')[-1])
@@ -29,10 +32,18 @@ def cached_file_delete(sender, instance, **kwargs):
instance.file.delete(False) instance.file.delete(False)
class LoggedModel(models.Model): class LoggingMixin:
logentries = GenericRelation('LogEntry') 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 .log import LogEntry
from .event import Event from .event import Event
@@ -41,7 +52,13 @@ class LoggedModel(models.Model):
event = self event = self
elif hasattr(self, 'event'): elif hasattr(self, 'event'):
event = 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: class Meta:
abstract = True abstract = True

View File

@@ -2,7 +2,7 @@ import random
import string import string
from datetime import datetime from datetime import datetime
from django.db import models from django.db import models, transaction
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from typing import List, Union from typing import List, Union
@@ -177,14 +177,6 @@ class Order(LoggedModel):
return True return True
return False # nothing there to modify 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]: def _can_be_paid(self) -> Union[bool, str]:
error_messages = { error_messages = {
'late': _("The payment is too late to be accepted."), 'late': _("The payment is too late to be accepted."),

View File

@@ -359,6 +359,7 @@ class BasePaymentProvider:
:param order: The order object :param order: The order object
""" """
self.order.log_action('pretix.base.order.refunded')
return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, ' return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
'please transfer the money back manually.') 'please transfer the money back manually.')
@@ -379,7 +380,9 @@ class BasePaymentProvider:
:param request: The HTTP request :param request: The HTTP request
:param order: The order object :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 ' messages.success(request, _('The order has been marked as refunded. Please transfer the money '
'back to the buyer manually.')) 'back to the buyer manually.'))
@@ -440,7 +443,9 @@ class FreeOrderProvider(BasePaymentProvider):
:param request: The HTTP request :param request: The HTTP request
:param order: The order object :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.')) messages.success(request, _('The order has been marked as refunded.'))
def is_allowed(self, request: HttpRequest) -> bool: def is_allowed(self, request: HttpRequest) -> bool:

View File

@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from typing import List from typing import List
from pretix.base.models import ( 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.payment import BasePaymentProvider
from pretix.base.services.mail import mail 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, 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 Marks an order as paid. This sets the payment provider, info and date and returns
the order object. 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 :param force: Whether this payment should be marked as paid even if no remaining
quota is available (default: ``False``). quota is available (default: ``False``).
:type force: boolean :type force: boolean
:param user: The user that performed the change
:raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False`` :raises Quota.QuotaExceededException: if the quota is exceeded and ``force`` is ``False``
""" """
with order.event.lock(): 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.payment_manual = manual
order.status = Order.STATUS_PAID order.status = Order.STATUS_PAID
order.save() 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) order_paid.send(order.event, order=order)
mail( mail(
@@ -78,6 +86,32 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
return order 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): class OrderError(Exception):
pass pass
@@ -154,6 +188,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], dt: d
payment_provider=payment_provider.identifier payment_provider=payment_provider.identifier
) )
OrderPosition.transform_cart_positions(positions, order) OrderPosition.transform_cart_positions(positions, order)
order.log_action('pretix.event.order.placed')
order_placed.send(event, order=order) order_placed.send(event, order=order)
return order return order

View File

@@ -121,7 +121,20 @@ class SettingsProxy:
def _flush(self) -> None: def _flush(self) -> None:
self._cached_obj = 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: 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): if as_type is not None and isinstance(value, as_type):
return value return value
elif value is None: elif value is None:
@@ -186,15 +199,14 @@ class SettingsProxy:
if value is None and default is not None: if value is None and default is not None:
value = default 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) return self._unserialize(value, as_type)
def __getitem__(self, key: str) -> Any: def __getitem__(self, key: str) -> Any:
return self.get(key) return self.get(key)
def __getattr__(self, key: str) -> Any: def __getattr__(self, key: str) -> Any:
if key.startswith('_'):
return super().__getattr__(key)
return self.get(key) return self.get(key)
def __setattr__(self, key: str, value: Any) -> None: def __setattr__(self, key: str, value: Any) -> None:

View File

@@ -62,6 +62,7 @@ def register(request):
timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE timezone=request.timezone if hasattr(request, 'timezone') else settings.TIME_ZONE
) )
user = authenticate(email=user.email, password=form.cleaned_data['password']) user = authenticate(email=user.email, password=form.cleaned_data['password'])
user.log_action('pretix.control.auth.user.created', user=user)
auth_login(request, user) auth_login(request, user)
return redirect('control:index') return redirect('control:index')
else: else:
@@ -90,6 +91,7 @@ class Forgot(TemplateView):
}, },
None, locale=user.locale 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.')) messages.success(request, _('We sent you an e-mail containing further instructions.'))
return redirect('control:auth.forgot') return redirect('control:auth.forgot')
else: else:
@@ -141,6 +143,7 @@ class Recover(TemplateView):
user.set_password(self.form.cleaned_data['password']) user.set_password(self.form.cleaned_data['password'])
user.save() user.save()
messages.success(request, _('You can now login using your new password.')) 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') return redirect('control:auth.login')
else: else:
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)

View File

@@ -3,6 +3,7 @@ from collections import OrderedDict
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from django.forms import modelformset_factory from django.forms import modelformset_factory
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
@@ -53,8 +54,17 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
context['sform'] = self.sform context['sform'] = self.sform
return context return context
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
self.sform.save() 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.')) messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form) return super().form_valid(form)
@@ -97,15 +107,20 @@ class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
plugins_active = self.object.get_plugins() plugins_active = self.object.get_plugins()
for key, value in request.POST.items(): with transaction.atomic():
if key.startswith("plugin:"): for key, value in request.POST.items():
module = key.split(":")[1] if key.startswith("plugin:"):
if value == "enable": module = key.split(":")[1]
plugins_active.append(module) if value == "enable":
else: self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
plugins_active.remove(module) data={'plugin': module})
self.object.plugins = ",".join(plugins_active) plugins_active.append(module)
self.object.save() 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.')) messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url()) return redirect(self.get_success_url())
@@ -159,11 +174,18 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
context['providers'] = self.provider_forms context['providers'] = self.provider_forms
return self.render_to_response(context) return self.render_to_response(context)
@transaction.atomic()
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success = True success = True
for provider in self.provider_forms: for provider in self.provider_forms:
if provider.form.is_valid(): 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() provider.form.save()
else: else:
success = False success = False
@@ -207,16 +229,29 @@ class TicketSettings(EventPermissionRequiredMixin, FormView):
form.prepare_fields() form.prepare_fields()
return form return form
@transaction.atomic()
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
success = True success = True
for provider in self.provider_forms: for provider in self.provider_forms:
if provider.form.is_valid(): if provider.form.is_valid():
provider.form.save() 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: else:
success = False success = False
form = self.get_form(self.get_form_class()) form = self.get_form(self.get_form_class())
if success and form.is_valid(): if success and form.is_valid():
form.save() 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.')) messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url()) return redirect(self.get_success_url())
else: else:
@@ -310,6 +345,7 @@ class EventPermissions(EventPermissionRequiredMixin, TemplateView):
ctx['add_form'] = self.add_form ctx['add_form'] = self.add_form
return ctx return ctx
@transaction.atomic()
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
if self.formset.is_valid() and self.add_form.is_valid(): if self.formset.is_valid() and self.add_form.is_valid():
if self.add_form.has_changed(): 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.')) messages.error(self.request, _('This user already has permissions for this event.'))
return self.get(*args, **kwargs) return self.get(*args, **kwargs)
self.add_form.save() 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: 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 form.instance.user_id == self.request.user.pk:
if not form.cleaned_data['can_change_permissions'] or form in self.formset.deleted_forms: 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.')) messages.error(self.request, _('You cannot remove your own permission to view this page.'))

View File

@@ -103,12 +103,14 @@ class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
except ItemCategory.DoesNotExist: except ItemCategory.DoesNotExist:
raise Http404(_("The requested product category does not exist.")) raise Http404(_("The requested product category does not exist."))
@transaction.atomic()
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
for item in self.object.items.all(): for item in self.object.items.all():
item.category = None item.category = None
item.save() item.save()
success_url = self.get_success_url() success_url = self.get_success_url()
self.object.log_action('pretix.event.category.deleted', user=self.request.user)
self.object.delete() self.object.delete()
messages.success(request, _('The selected category has been deleted.')) messages.success(request, _('The selected category has been deleted.'))
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -136,8 +138,15 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
except ItemCategory.DoesNotExist: except ItemCategory.DoesNotExist:
raise Http404(_("The requested product category does not exist.")) raise Http404(_("The requested product category does not exist."))
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) 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) return super().form_valid(form)
def get_success_url(self) -> str: def get_success_url(self) -> str:
@@ -160,10 +169,13 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
'event': self.request.event.slug, 'event': self.request.event.slug,
}) })
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
form.instance.event = self.request.event form.instance.event = self.request.event
messages.success(self.request, _('The new category has been created.')) 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): class CategoryList(ListView):
@@ -248,9 +260,11 @@ class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
context['dependent'] = list(self.get_object().items.all()) context['dependent'] = list(self.get_object().items.all())
return context return context
@transaction.atomic()
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success_url = self.get_success_url() success_url = self.get_success_url()
self.object.log_action(action='pretix.event.question.deleted', user=request.user)
self.object.delete() self.object.delete()
messages.success(request, _('The selected question has been deleted.')) messages.success(request, _('The selected question has been deleted.'))
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -277,7 +291,14 @@ class QuestionUpdate(EventPermissionRequiredMixin, UpdateView):
except Question.DoesNotExist: except Question.DoesNotExist:
raise Http404(_("The requested question does not exist.")) raise Http404(_("The requested question does not exist."))
@transaction.atomic()
def form_valid(self, form): 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.')) messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form) return super().form_valid(form)
@@ -306,9 +327,12 @@ class QuestionCreate(EventPermissionRequiredMixin, CreateView):
'event': self.request.event.slug, 'event': self.request.event.slug,
}) })
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('The new question has been created.')) 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): class QuotaList(ListView):
@@ -378,10 +402,13 @@ class QuotaCreate(EventPermissionRequiredMixin, QuotaEditorMixin, CreateView):
'event': self.request.event.slug, 'event': self.request.event.slug,
}) })
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
form.instance.event = self.request.event form.instance.event = self.request.event
messages.success(self.request, _('The new quota has been created.')) 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): class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
@@ -399,8 +426,15 @@ class QuotaUpdate(EventPermissionRequiredMixin, QuotaEditorMixin, UpdateView):
except Quota.DoesNotExist: except Quota.DoesNotExist:
raise Http404(_("The requested quota does not exist.")) raise Http404(_("The requested quota does not exist."))
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) 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) return super().form_valid(form)
def get_success_url(self) -> str: def get_success_url(self) -> str:
@@ -429,9 +463,11 @@ class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
context['dependent'] = list(self.get_object().items.all()) context['dependent'] = list(self.get_object().items.all())
return context return context
@transaction.atomic()
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
success_url = self.get_success_url() success_url = self.get_success_url()
self.object.log_action(action='pretix.event.quota.deleted', user=request.user)
self.object.delete() self.object.delete()
messages.success(self.request, _('The selected quota has been deleted.')) messages.success(self.request, _('The selected quota has been deleted.'))
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -471,9 +507,12 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
'item': self.object.id, 'item': self.object.id,
}) })
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) 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): def get_form_kwargs(self):
""" """
@@ -497,8 +536,15 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
'item': self.get_object().id, 'item': self.get_object().id,
}) })
@transaction.atomic()
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) 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) return super().form_valid(form)
@@ -543,13 +589,31 @@ class ItemProperties(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
for f in formset: for f in formset:
f.instance.event = self.request.event f.instance.event = self.request.event
f.instance.item = self.get_object() f.instance.item = self.get_object()
is_created = not f.instance.pk
f.instance.save() 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: for n in f.nested:
print(n.deleted_forms, n.ordered_forms, n.extra_forms)
for fn in n.deleted_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.delete()
fn.instance.pk = None fn.instance.pk = None
@@ -557,8 +621,24 @@ class ItemProperties(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
fn.instance.position = i fn.instance.position = i
fn.instance.prop = f.instance fn.instance.prop = f.instance
fn.save() 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.')) messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url()) return redirect(self.get_success_url())
@@ -700,6 +780,13 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
for form in self.forms_flat: for form in self.forms_flat:
if form.is_valid() and form.has_changed(): if form.is_valid() and form.has_changed():
form.save() 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: if hasattr(form.instance, 'creation') and form.instance.creation:
# We need this special 'creation' field set to true in get_form # We need this special 'creation' field set to true in get_form
# for newly created items as cleanerversion does already set the # 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.")) raise Http404(_("The requested product does not exist."))
return self.object return self.object
@transaction.atomic
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
success_url = self.get_success_url() success_url = self.get_success_url()
if self.is_allowed(): if self.is_allowed():
self.get_object().log_action('pretix.event.item.deleted', user=self.request.user)
self.get_object().delete() self.get_object().delete()
messages.success(request, _('The selected product has been deleted.')) messages.success(request, _('The selected product has been deleted.'))
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
@@ -768,6 +857,9 @@ class ItemDelete(EventPermissionRequiredMixin, DeleteView):
o = self.get_object() o = self.get_object()
o.active = False o.active = False
o.save() 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.')) messages.success(request, _('The selected product has been deactivated.'))
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)

View File

@@ -17,7 +17,7 @@ from pretix.base.models import (
) )
from pretix.base.services import tickets from pretix.base.services import tickets
from pretix.base.services.export import export 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.services.stats import order_overview
from pretix.base.signals import ( from pretix.base.signals import (
register_data_exporters, register_payment_providers, register_data_exporters, register_payment_providers,
@@ -136,8 +136,8 @@ class OrderDetail(OrderView):
def keyfunc(pos): def keyfunc(pos):
if (pos.item.admission and self.request.event.settings.attendee_names_asked) \ if (pos.item.admission and self.request.event.settings.attendee_names_asked) \
or pos.item.questions.all(): or pos.item.questions.all():
return pos.id, "", "", "" return pos.id, 0, 0, 0
return "", pos.item_id, pos.variation_id, pos.price return 0, pos.item_id, pos.variation_id, pos.price
positions = [] positions = []
for k, g in groupby(sorted(list(cartpos), key=keyfunc), key=keyfunc): 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', '') to = self.request.POST.get('status', '')
if self.order.status == 'n' and to == 'p': if self.order.status == 'n' and to == 'p':
try: try:
mark_order_paid(self.order, manual=True) mark_order_paid(self.order, manual=True, user=self.request.user)
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
messages.error(self.request, str(e)) messages.error(self.request, str(e))
else: else:
messages.success(self.request, _('The order has been marked as paid.')) messages.success(self.request, _('The order has been marked as paid.'))
elif self.order.status == 'n' and to == 'c': elif self.order.status == 'n' and to == 'c':
self.order.status = Order.STATUS_CANCELLED cancel_order(self.order, user=self.request.user)
self.order.save()
messages.success(self.request, _('The order has been cancelled.')) messages.success(self.request, _('The order has been cancelled.'))
elif self.order.status == 'p' and to == 'n': elif self.order.status == 'p' and to == 'n':
self.order.status = Order.STATUS_PENDING self.order.status = Order.STATUS_PENDING
self.order.payment_manual = True self.order.payment_manual = True
self.order.save() 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.')) messages.success(self.request, _('The order has been marked as not paid.'))
elif self.order.status == 'p' and to == 'r': elif self.order.status == 'p' and to == 'r':
ret = self.payment_provider.order_control_refund_perform(self.request, self.order) 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 self.form.is_valid():
if oldvalue > now(): if oldvalue > now():
messages.success(self.request, _('The payment term has been changed.')) 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() self.form.save()
else: else:
try: try:
@@ -254,6 +257,9 @@ class OrderExtend(OrderView):
is_available = self.order._is_still_available() is_available = self.order._is_still_available()
if is_available is True: if is_available is True:
self.form.save() 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.')) messages.success(self.request, _('The payment term has been changed.'))
else: else:
messages.error(self.request, is_available) messages.error(self.request, is_available)

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext as __, ugettext_lazy as _
from pretix.base.models import Quota from pretix.base.models import Quota
from pretix.base.payment import BasePaymentProvider 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 from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger('pretix.plugins.paypal') logger = logging.getLogger('pretix.plugins.paypal')
@@ -218,7 +218,7 @@ class Paypal(BasePaymentProvider):
payment_info = None payment_info = None
if not payment_info: if not payment_info:
order.mark_refunded() mark_order_refunded(order)
messages.warning(request, _('We were unable to transfer the money back automatically. ' messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.')) 'Please get in touch with the customer and transfer it back manually.'))
return return
@@ -231,11 +231,11 @@ class Paypal(BasePaymentProvider):
refund = sale.refund({}) refund = sale.refund({})
if not refund.success(): 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. ' messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.')) 'Please get in touch with the customer and transfer it back manually.'))
else: else:
sale = paypalrestsdk.Payment.find(payment_info['id']) 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.payment_info = json.dumps(sale.to_dict())
order.save() order.save()

View File

@@ -27,12 +27,15 @@ class SenderView(EventPermissionRequiredMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
orders = Order.objects.filter( orders = Order.objects.filter(
event=self.request.event, status__in=form.cleaned_data['sendto'] 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: self.request.event.log_action('pretix.plugins.sendmail.sent', user=self.request.user, data=dict(
mail(u.email, form.cleaned_data['subject'], form.cleaned_data['message'], form.cleaned_data))
None, self.request.event, locale=u.locale)
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.')) messages.success(self.request, _('Your message will be sent to the selected users.'))

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Quota from pretix.base.models import Quota
from pretix.base.payment import BasePaymentProvider 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 from pretix.helpers.urls import build_absolute_uri
logger = logging.getLogger('pretix.plugins.stripe') logger = logging.getLogger('pretix.plugins.stripe')
@@ -157,7 +157,7 @@ class Stripe(BasePaymentProvider):
payment_info = None payment_info = None
if not payment_info: 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. ' messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.')) 'Please get in touch with the customer and transfer it back manually.'))
return return
@@ -173,10 +173,10 @@ class Stripe(BasePaymentProvider):
'support if the problem persists.')) 'support if the problem persists.'))
logger.error('Stripe error: %s' % str(err)) logger.error('Stripe error: %s' % str(err))
except stripe.error.StripeError: except stripe.error.StripeError:
order.mark_refunded() mark_order_refunded(order)
messages.warning(request, _('We were unable to transfer the money back automatically. ' messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.')) 'Please get in touch with the customer and transfer it back manually.'))
else: else:
order = order.mark_refunded() order = mark_order_refunded(order)
order.payment_info = str(ch) order.payment_info = str(ch)
order.save() order.save()

View File

@@ -7,6 +7,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from pretix.base.models import Event, Order from pretix.base.models import Event, Order
from pretix.base.services.orders import mark_order_refunded
from pretix.plugins.stripe.payment import Stripe from pretix.plugins.stripe.payment import Stripe
logger = logging.getLogger('pretix.plugins.stripe') logger = logging.getLogger('pretix.plugins.stripe')
@@ -42,6 +43,8 @@ def webhook(request):
prov = Stripe(event) prov = Stripe(event)
prov._init_api() prov._init_api()
order.log_action('pretix.plugins.stripe.event', data=event_json)
try: try:
charge = stripe.Charge.retrieve(charge['id']) charge = stripe.Charge.retrieve(charge['id'])
except stripe.error.StripeError as err: except stripe.error.StripeError as err:
@@ -49,6 +52,6 @@ def webhook(request):
return HttpResponse('StripeError', status=500) return HttpResponse('StripeError', status=500)
if charge['refunds']['total_count'] > 0 and order.status == Order.STATUS_PAID: if charge['refunds']['total_count'] > 0 and order.status == Order.STATUS_PAID:
order.mark_refunded() mark_order_refunded(order)
return HttpResponse(status=200) return HttpResponse(status=200)

View File

@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from pretix.base.models import CachedFile, CachedTicket, Order, OrderPosition 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.services.tickets import generate
from pretix.base.signals import ( from pretix.base.signals import (
register_payment_providers, register_ticket_outputs, register_payment_providers, register_ticket_outputs,
@@ -215,6 +216,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
messages.error(self.request, messages.error(self.request,
_("We had difficulties processing your input. Please review the errors below.")) _("We had difficulties processing your input. Please review the errors below."))
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
self.order.log_action('pretix.event.order.modified')
return redirect(self.get_order_url()) return redirect(self.get_order_url())
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -251,8 +253,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.order.status = Order.STATUS_CANCELLED cancel_order(self.order)
self.order.save()
return redirect(self.get_order_url()) return redirect(self.get_order_url())
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View File

@@ -12,7 +12,6 @@ from pretix.base.settings import SettingsSandbox
class SettingsTestCase(TestCase): class SettingsTestCase(TestCase):
def setUp(self): def setUp(self):
settings.DEFAULTS['test_default'] = { settings.DEFAULTS['test_default'] = {
'default': 'def', 'default': 'def',
@@ -149,6 +148,7 @@ class SettingsTestCase(TestCase):
def test_serialize_unknown(self): def test_serialize_unknown(self):
class Type: class Type:
pass pass
try: try:
self._test_serialization(Type(), Type) self._test_serialization(Type(), Type)
self.assertTrue(False, 'No exception thrown!') self.assertTrue(False, 'No exception thrown!')
@@ -195,3 +195,23 @@ class SettingsTestCase(TestCase):
self.assertIsNone(sandbox.bar) self.assertIsNone(sandbox.bar)
self.assertIsNone(sandbox['baz']) 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