Add meta_data for items (#1576)

* PoC for ItemMetaProperties/Values

* Missing is_valid

* ItemMetaProperties/Values in editable via API, cloneable

* Tests

* Add Docs

* Fix import order

* Fix another import sorting...

* Typeahead for ItemMetaValues

* Test for editing event-objects

* Fix typeahead permission checks

* Further access restriction

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
This commit is contained in:
Martin Gross
2020-02-26 15:06:25 +01:00
committed by GitHub
parent dd1e5fa929
commit 76aaf61e19
22 changed files with 573 additions and 25 deletions

View File

@@ -11,6 +11,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.files import File
from django.db import transaction
from django.db.models import ProtectedError
from django.forms import inlineformset_factory
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
JsonResponse,
@@ -39,9 +40,9 @@ from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.forms.event import (
CancelSettingsForm, CommentForm, EventDeleteForm, EventMetaValueForm,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, QuickSetupForm,
QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm,
ItemMetaPropertyForm, MailSettingsForm, PaymentSettingsForm, ProviderForm,
QuickSetupForm, QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -51,6 +52,7 @@ from pretix.multidomain.urlreverse import get_domain
from pretix.plugins.stripe.payment import StripeSettingsHolder
from pretix.presale.style import regenerate_css
from ...base.models.items import ItemMetaProperty
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -137,6 +139,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform
context['meta_forms'] = self.meta_forms
context['formset'] = self.formset
return context
@transaction.atomic
@@ -144,6 +147,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
self._save_decoupled(self.sform)
self.sform.save()
self.save_meta()
self.save_formset(self.object)
change_css = False
if self.sform.has_changed():
@@ -183,7 +187,8 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and self.sform.is_valid() and all([f.is_valid() for f in self.meta_forms]):
if form.is_valid() and self.sform.is_valid() and all([f.is_valid() for f in self.meta_forms]) and \
self.formset.is_valid():
# reset timezone
zone = timezone(self.sform.cleaned_data['timezone'])
event = form.instance
@@ -200,6 +205,33 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
def reset_timezone(tz, dt):
return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None
@cached_property
def formset(self):
formsetclass = inlineformset_factory(
Event, ItemMetaProperty,
form=ItemMetaPropertyForm, can_order=False, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
instance=self.object, queryset=self.object.item_meta_properties.all())
def save_formset(self, obj):
for form in self.formset.initial_forms:
if form in self.formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.delete()
form.instance.pk = None
elif form.has_changed():
form.save()
for form in self.formset.extra_forms:
if not form.has_changed():
continue
if self.formset._should_delete_form(form):
continue
form.instance.event = obj
form.save()
class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event

View File

@@ -30,13 +30,14 @@ from pretix.base.models import (
QuestionAnswer, QuestionOption, Quota, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemUpdateForm, ItemVariationForm,
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemUpdateForm,
ItemVariationForm, ItemVariationsFormSet, QuestionForm, QuestionOptionForm,
QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -939,6 +940,41 @@ class ItemDetailMixin(SingleObjectMixin):
raise Http404(_("The requested item does not exist."))
class MetaDataEditorMixin:
meta_form = ItemMetaValueForm
meta_model = ItemMetaValue
@cached_property
def meta_forms(self):
if hasattr(self, 'object') and self.object:
val_instances = {
v.property_id: v for v in self.object.meta_values.all()
}
else:
val_instances = {}
formlist = []
for p in self.request.event.item_meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
return formlist
def _make_meta_form(self, p, val_instances):
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
instance=val_instances.get(p.pk, self.meta_model(property=p, item=self.object)),
data=(self.request.POST if self.request.method == "POST" else None)
)
def save_meta(self):
for f in self.meta_forms:
if f.cleaned_data.get('value'):
f.save()
elif f.instance and f.instance.pk:
f.instance.delete()
class ItemCreate(EventPermissionRequiredMixin, CreateView):
form_class = ItemCreateForm
template_name = 'pretixcontrol/item/create.html'
@@ -985,7 +1021,7 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
return super().form_invalid(form)
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateView):
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
form_class = ItemUpdateForm
template_name = 'pretixcontrol/item/index.html'
permission = 'can_change_items'
@@ -1038,7 +1074,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
def post(self, request, *args, **kwargs):
self.get_object()
form = self.get_form()
if self.is_valid(form):
if self.is_valid(form) and all([f.is_valid() for f in self.meta_forms]):
return self.form_valid(form)
else:
return self.form_invalid(form)
@@ -1088,6 +1124,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
@transaction.atomic
def form_valid(self, form):
self.save_meta()
messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
data = {
@@ -1137,6 +1174,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['plugin_forms'] = self.plugin_forms
ctx['meta_forms'] = self.meta_forms
ctx['formsets'] = self.formsets
if not ctx['item'].active and ctx['item'].bundled_with.count() > 0:

View File

@@ -13,7 +13,8 @@ from django.utils.timezone import make_aware
from django.utils.translation import pgettext, ugettext as _
from pretix.base.models import (
EventMetaProperty, EventMetaValue, Order, Organizer, User, Voucher,
EventMetaProperty, EventMetaValue, ItemMetaProperty, ItemMetaValue, Order,
Organizer, User, Voucher,
)
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import event_permission_required
@@ -589,3 +590,46 @@ def meta_values(request):
for v in sorted(set(defaults.values_list('default', flat=True)[:10]) | set(matches.values_list('value', flat=True)[:10]))
]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
matches = ItemMetaValue.objects.filter(
value__icontains=q,
property__name=propname
)
defaults = ItemMetaProperty.objects.filter(
name=propname,
default__icontains=q
)
organizer = get_object_or_404(Organizer, slug=organizer)
if not request.user.has_organizer_permission(organizer, request=request):
raise PermissionDenied()
defaults = defaults.filter(event__organizer_id=organizer.pk)
matches = matches.filter(item__event__organizer_id=organizer.pk)
all_access = (
request.user.has_active_staff_session(request.session.session_key)
or request.user.teams.filter(all_events=True, organizer=organizer, can_change_items=True).exists()
)
if not all_access:
defaults = matches.filter(
event__id__in=request.user.teams.filter(can_change_items=True).values_list(
'limit_events__id', flat=True
)
)
matches = matches.filter(
item__event__id__in=request.user.teams.filter(can_change_items=True).values_list(
'limit_events__id', flat=True
)
)
return JsonResponse({
'results': [
{'name': v, 'id': v}
for v in sorted(set(defaults.values_list('default', flat=True)[:10]) | set(matches.values_list('value', flat=True)[:10]))
]
})