text: #
  # This file is part of pretix (Community Edition).
  #
  # Copyright (C) 2014-2020 Raphael Michel and contributors
  # Copyright (C) 2020-2021 rami.io GmbH and contributors
  #
  # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
  # Public License as published by the Free Software Foundation in version 3 of the License.
  #
  # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
  # applicable granting you additional permissions and placing additional restrictions on your usage of this software.
  # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
  # this file, see <https://pretix.eu/about/en/license>.
  #
  # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
  # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
  # details.
  #
  # You should have received a copy of the GNU Affero General Public License along with this program.  If not, see
  # <https://www.gnu.org/licenses/>.
  #
  
  # This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
  # the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
  #
  # This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
  # full history of changes and contributors is available at <https://github.com/pretix/pretix>.
  #
  # This file contains Apache-licensed contributions copyrighted by: Adam K. Sumner, Enrique Saez, Jakob Schnell, Sohalt,
  # Tobias Kunze, Ture Gjørup
  #
  # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
  # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  # License for the specific language governing permissions and limitations under the License.
  
  import json
  from collections import OrderedDict, namedtuple
  from json.decoder import JSONDecodeError
  
  from django.contrib import messages
  from django.core.exceptions import PermissionDenied
  from django.core.files import File
  from django.db import transaction
  from django.db.models import (
      Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q,
  )
  from django.forms.models import inlineformset_factory
  from django.http import (
      Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
  )
  from django.shortcuts import redirect
  from django.urls import resolve, reverse
  from django.utils.functional import cached_property
  from django.utils.timezone import now
  from django.utils.translation import gettext, gettext_lazy as _
  from django.views.generic import ListView
  from django.views.generic.detail import DetailView, SingleObjectMixin
  from django.views.generic.edit import DeleteView
  from django_countries.fields import Country
  
  from pretix.api.serializers.item import (
      ItemAddOnSerializer, ItemBundleSerializer, ItemVariationSerializer,
  )
  from pretix.base.forms import I18nFormSet
  from pretix.base.models import (
      CartPosition, Item, ItemCategory, ItemVariation, Order, Question,
      QuestionAnswer, QuestionOption, Quota, Voucher,
  )
  from pretix.base.models.event import SubEvent
  from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
  from pretix.base.services.quotas import QuotaAvailability
  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, ItemMetaValueForm, ItemUpdateForm,
      ItemVariationForm, ItemVariationsFormSet, QuestionForm, QuestionOptionForm,
      QuotaForm,
  )
  from pretix.control.permissions import (
      EventPermissionRequiredMixin, event_permission_required,
  )
  from pretix.control.signals import item_forms, item_formsets
  from pretix.helpers.models import modelcopy
  
  from ...base.channels import get_all_sales_channels
  from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
  
  
  class ItemList(ListView):
      model = Item
      context_object_name = 'items'
      # paginate_by = 30
      # Pagination is disabled as it is very unlikely to be necessary
      # here and could cause problems with the "reorder-within-category" feature
      template_name = 'pretixcontrol/items/index.html'
  
      def get_queryset(self):
          return Item.objects.filter(
              event=self.request.event
          ).annotate(
              var_count=Count('variations')
          ).prefetch_related("category").order_by(
              'category__position', 'category', 'position'
          )
  
      def get_context_data(self, **kwargs):
          ctx = super().get_context_data(**kwargs)
          ctx['sales_channels'] = get_all_sales_channels()
          return ctx
  
  
  def item_move(request, item, up=True):
      """
      This is a helper function to avoid duplicating code in item_move_up and
      item_move_down. It takes an item and a direction and then tries to bring
      all items for this category in a new order.
      """
      try:
          item = request.event.items.get(
              id=item
          )
      except Item.DoesNotExist:
          raise Http404(_("The requested product does not exist."))
      items = list(request.event.items.filter(category=item.category).order_by("position"))
  
      index = items.index(item)
      if index != 0 and up:
          items[index - 1], items[index] = items[index], items[index - 1]
      elif index != len(items) - 1 and not up:
          items[index + 1], items[index] = items[index], items[index + 1]
  
      for i, item in enumerate(items):
          if item.position != i:
              item.position = i
              item.save()
      messages.success(request, _('The order of items has been updated.'))
  
  
  @event_permission_required("can_change_items")
  def item_move_up(request, organizer, event, item):
      item_move(request, item, up=True)
      return redirect('control:event.items',
                      organizer=request.event.organizer.slug,
                      event=request.event.slug)
  
  
  @event_permission_required("can_change_items")
  def item_move_down(request, organizer, event, item):
      item_move(request, item, up=False)
      return redirect('control:event.items',
                      organizer=request.event.organizer.slug,
                      event=request.event.slug)
  
  
  class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
      model = ItemCategory
      form_class = CategoryForm
      template_name = 'pretixcontrol/items/category_delete.html'
      permission = 'can_change_items'
      context_object_name = 'category'
  
      def get_object(self, queryset=None) -> ItemCategory:
          try:
              return self.request.event.categories.get(
                  id=self.kwargs['category']
              )
          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)
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.categories', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
  
  class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
      model = ItemCategory
      form_class = CategoryForm
      template_name = 'pretixcontrol/items/category.html'
      permission = 'can_change_items'
      context_object_name = 'category'
  
      def get_object(self, queryset=None) -> ItemCategory:
          url = resolve(self.request.path_info)
          try:
              return self.request.event.categories.get(
                  id=url.kwargs['category']
              )
          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:
          return reverse('control:event.items.categories', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
  
  class CategoryCreate(EventPermissionRequiredMixin, CreateView):
      model = ItemCategory
      form_class = CategoryForm
      template_name = 'pretixcontrol/items/category.html'
      permission = 'can_change_items'
      context_object_name = 'category'
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.categories', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
      @cached_property
      def copy_from(self):
          if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
              try:
                  return self.request.event.categories.get(pk=self.request.GET.get("copy_from"))
              except ItemCategory.DoesNotExist:
                  pass
  
      def get_form_kwargs(self):
          kwargs = super().get_form_kwargs()
  
          if self.copy_from:
              i = modelcopy(self.copy_from)
              i.pk = None
              kwargs['instance'] = i
          else:
              kwargs['instance'] = ItemCategory(event=self.request.event)
          return kwargs
  
      @transaction.atomic
      def form_valid(self, form):
          form.instance.event = self.request.event
          messages.success(self.request, _('The new category has been created.'))
          ret = super().form_valid(form)
          form.instance.log_action('pretix.event.category.added', data=dict(form.cleaned_data), user=self.request.user)
          return ret
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
  
  class CategoryList(PaginationMixin, ListView):
      model = ItemCategory
      context_object_name = 'categories'
      template_name = 'pretixcontrol/items/categories.html'
  
      def get_queryset(self):
          return self.request.event.categories.all()
  
  
  def category_move(request, category, up=True):
      """
      This is a helper function to avoid duplicating code in category_move_up and
      category_move_down. It takes a category and a direction and then tries to bring
      all categories for this event in a new order.
      """
      try:
          category = request.event.categories.get(
              id=category
          )
      except ItemCategory.DoesNotExist:
          raise Http404(_("The requested product category does not exist."))
      categories = list(request.event.categories.order_by("position"))
  
      index = categories.index(category)
      if index != 0 and up:
          categories[index - 1], categories[index] = categories[index], categories[index - 1]
      elif index != len(categories) - 1 and not up:
          categories[index + 1], categories[index] = categories[index], categories[index + 1]
  
      for i, cat in enumerate(categories):
          if cat.position != i:
              cat.position = i
              cat.save()
      messages.success(request, _('The order of categories has been updated.'))
  
  
  @event_permission_required("can_change_items")
  def category_move_up(request, organizer, event, category):
      category_move(request, category, up=True)
      return redirect('control:event.items.categories',
                      organizer=request.event.organizer.slug,
                      event=request.event.slug)
  
  
  @event_permission_required("can_change_items")
  def category_move_down(request, organizer, event, category):
      category_move(request, category, up=False)
      return redirect('control:event.items.categories',
                      organizer=request.event.organizer.slug,
                      event=request.event.slug)
  
  
  FakeQuestion = namedtuple(
      'FakeQuestion', 'id question position required'
  )
  
  
  class QuestionList(ListView):
      model = Question
      context_object_name = 'questions'
      template_name = 'pretixcontrol/items/questions.html'
  
      def get_queryset(self):
          return self.request.event.questions.prefetch_related('items')
  
      def get_context_data(self, **kwargs):
          ctx = super().get_context_data(**kwargs)
          ctx['questions'] = list(ctx['questions'])
  
          if self.request.event.settings.attendee_names_asked:
              ctx['questions'].append(
                  FakeQuestion(
                      id='attendee_name_parts',
                      question=_('Attendee name'),
                      position=self.request.event.settings.system_question_order.get(
                          'attendee_name_parts', 0
                      ),
                      required=self.request.event.settings.attendee_names_required,
                  )
              )
  
          if self.request.event.settings.attendee_emails_asked:
              ctx['questions'].append(
                  FakeQuestion(
                      id='attendee_email',
                      question=_('Attendee email'),
                      position=self.request.event.settings.system_question_order.get(
                          'attendee_email', 0
                      ),
                      required=self.request.event.settings.attendee_emails_required,
                  )
              )
  
          if self.request.event.settings.attendee_emails_asked:
              ctx['questions'].append(
                  FakeQuestion(
                      id='company',
                      question=_('Company'),
                      position=self.request.event.settings.system_question_order.get(
                          'company', 0
                      ),
                      required=self.request.event.settings.attendee_company_required,
                  )
              )
  
          if self.request.event.settings.attendee_addresses_asked:
              ctx['questions'].append(
                  FakeQuestion(
                      id='street',
                      question=_('Street'),
                      position=self.request.event.settings.system_question_order.get(
                          'street', 0
                      ),
                      required=self.request.event.settings.attendee_addresses_required,
                  )
              )
              ctx['questions'].append(
                  FakeQuestion(
                      id='zipcode',
                      question=_('ZIP code'),
                      position=self.request.event.settings.system_question_order.get(
                          'zipcode', 0
                      ),
                      required=self.request.event.settings.attendee_addresses_required,
                  )
              )
              ctx['questions'].append(
                  FakeQuestion(
                      id='city',
                      question=_('City'),
                      position=self.request.event.settings.system_question_order.get(
                          'city', 0
                      ),
                      required=self.request.event.settings.attendee_addresses_required,
                  )
              )
              ctx['questions'].append(
                  FakeQuestion(
                      id='country',
                      question=_('Country'),
                      position=self.request.event.settings.system_question_order.get(
                          'country', 0
                      ),
                      required=self.request.event.settings.attendee_addresses_required,
                  )
              )
  
          ctx['questions'].sort(key=lambda q: q.position)
          return ctx
  
  
  @transaction.atomic
  @event_permission_required("can_change_items")
  def reorder_questions(request, organizer, event):
      try:
          ids = json.loads(request.body.decode('utf-8'))['ids']
      except (JSONDecodeError, KeyError, ValueError):
          return HttpResponseBadRequest("expected JSON: {ids:[]}")
  
      input_questions = request.event.questions.filter(id__in=[i for i in ids if i.isdigit()])
  
      if input_questions.count() != len([i for i in ids if i.isdigit()]):
          raise Http404(_("Some of the provided question ids are invalid."))
  
      if input_questions.count() != request.event.questions.count():
          raise Http404(_("Not all questions have been selected."))
  
      for q in input_questions:
          pos = ids.index(str(q.pk))
          if pos != q.position:  # Save unneccessary UPDATE queries
              q.position = pos
              q.save(update_fields=['position'])
  
      system_question_order = {}
      for s in ('attendee_name_parts', 'attendee_email', 'company', 'street', 'zipcode', 'city', 'country'):
          if s in ids:
              system_question_order[s] = ids.index(s)
          else:
              system_question_order[s] = -1
      request.event.settings.system_question_order = system_question_order
  
      return HttpResponse()
  
  
  class QuestionDelete(EventPermissionRequiredMixin, DeleteView):
      model = Question
      template_name = 'pretixcontrol/items/question_delete.html'
      permission = 'can_change_items'
      context_object_name = 'question'
  
      def get_object(self, queryset=None) -> Question:
          try:
              return self.request.event.questions.get(
                  id=self.kwargs['question']
              )
          except Question.DoesNotExist:
              raise Http404(_("The requested question does not exist."))
  
      def get_context_data(self, *args, **kwargs) -> dict:
          context = super().get_context_data(*args, **kwargs)
          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)
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.questions', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
  
  class QuestionMixin:
      @cached_property
      def formset(self):
          formsetclass = inlineformset_factory(
              Question, QuestionOption,
              form=QuestionOptionForm, formset=I18nFormSet,
              can_order=True, can_delete=True, extra=0
          )
          return formsetclass(self.request.POST if self.request.method == "POST" else None,
                              queryset=(QuestionOption.objects.filter(question=self.object)
                                        if self.object else QuestionOption.objects.none()),
                              event=self.request.event)
  
      def save_formset(self, obj):
          if self.formset.is_valid():
              for form in self.formset.initial_forms:
                  if form in self.formset.deleted_forms:
                      if not form.instance.pk:
                          continue
                      obj.log_action(
                          'pretix.event.question.option.deleted', user=self.request.user, data={
                              'id': form.instance.pk
                          }
                      )
                      form.instance.delete()
                      form.instance.pk = None
  
              forms = self.formset.ordered_forms + [
                  ef for ef in self.formset.extra_forms
                  if ef not in self.formset.ordered_forms and ef not in self.formset.deleted_forms
              ]
              for i, form in enumerate(forms):
                  form.instance.position = i
                  form.instance.question = obj
                  created = not form.instance.pk
                  form.save()
                  if form.has_changed():
                      change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
                      change_data['id'] = form.instance.pk
                      obj.log_action(
                          'pretix.event.question.option.added' if created else
                          'pretix.event.question.option.changed',
                          user=self.request.user, data=change_data
                      )
  
              return True
          return False
  
      def get_context_data(self, **kwargs):
          ctx = super().get_context_data(**kwargs)
          ctx['formset'] = self.formset
          return ctx
  
  
  class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingView, DetailView):
      model = Question
      template_name = 'pretixcontrol/items/question.html'
      permission = 'can_change_items'
      template_name_field = 'question'
  
      def get_answer_statistics(self):
          qs = QuestionAnswer.objects.filter(
              question=self.object, orderposition__isnull=False,
              orderposition__order__event=self.request.event
          )
          s = self.request.GET.get("status", "np")
          if s != "":
              if s == 'o':
                  qs = qs.filter(orderposition__order__status=Order.STATUS_PENDING,
                                 orderposition__order__expires__lt=now().replace(hour=0, minute=0, second=0))
              elif s == 'np':
                  qs = qs.filter(orderposition__order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
              elif s == 'ne':
                  qs = qs.filter(orderposition__order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
              else:
                  qs = qs.filter(orderposition__order__status=s)
  
          if s not in (Order.STATUS_CANCELED, ""):
              qs = qs.filter(orderposition__canceled=False)
          if self.request.GET.get("item", "") != "":
              i = self.request.GET.get("item", "")
              qs = qs.filter(orderposition__item_id__in=(i,))
  
          if self.object.type == Question.TYPE_FILE:
              qs = [
                  {
                      'answer': gettext('File uploaded'),
                      'count': qs.filter(file__isnull=False).count(),
                  }
              ]
          elif self.object.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
              qs = qs.order_by('options').values('options', 'options__answer') \
                  .annotate(count=Count('id')).order_by('-count')
              for a in qs:
                  a['alink'] = a['options']
                  a['answer'] = str(a['options__answer'])
                  del a['options__answer']
          elif self.object.type in (Question.TYPE_TIME, Question.TYPE_DATE, Question.TYPE_DATETIME):
              qs = qs.order_by('answer')
              model_cache = {a.answer: a for a in qs}
              qs = qs.values('answer').annotate(count=Count('id')).order_by('answer')
              for a in qs:
                  a['alink'] = a['answer']
                  a['answer'] = str(model_cache[a['answer']])
          else:
              qs = qs.order_by('answer').values('answer').annotate(count=Count('id')).order_by('-count')
  
              if self.object.type == Question.TYPE_BOOLEAN:
                  for a in qs:
                      a['alink'] = a['answer']
                      a['answer_bool'] = a['answer'] == 'True'
                      a['answer'] = gettext('Yes') if a['answer'] == 'True' else gettext('No')
              elif self.object.type == Question.TYPE_COUNTRYCODE:
                  for a in qs:
                      a['alink'] = a['answer']
                      a['answer'] = Country(a['answer']).name or a['answer']
  
          r = list(qs)
          total = sum(a['count'] for a in r)
          for a in r:
              a['percentage'] = (a['count'] / total * 100.) if total else 0
          return r
  
      def get_context_data(self, **kwargs):
          ctx = super().get_context_data()
          ctx['items'] = self.object.items.all()
          stats = self.get_answer_statistics()
          ctx['stats'] = stats
          ctx['stats_json'] = json.dumps(stats)
          return ctx
  
      def get_object(self, queryset=None) -> Question:
          try:
              return self.request.event.questions.get(
                  id=self.kwargs['question']
              )
          except Question.DoesNotExist:
              raise Http404(_("The requested question does not exist."))
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.questions', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
  
  class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView):
      model = Question
      form_class = QuestionForm
      template_name = 'pretixcontrol/items/question_edit.html'
      permission = 'can_change_items'
      context_object_name = 'question'
  
      def get_object(self, queryset=None) -> Question:
          try:
              return self.request.event.questions.get(
                  id=self.kwargs['question']
              )
          except Question.DoesNotExist:
              raise Http404(_("The requested question does not exist."))
  
      @transaction.atomic
      def form_valid(self, form):
          if form.cleaned_data.get('type') in ('M', 'C'):
              if not self.save_formset(self.get_object()):
                  return self.get(self.request, *self.args, **self.kwargs)
  
          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)
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.questions', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
  
  class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
      model = Question
      form_class = QuestionForm
      template_name = 'pretixcontrol/items/question_edit.html'
      permission = 'can_change_items'
      context_object_name = 'question'
  
      def get_form_kwargs(self):
          kwargs = super().get_form_kwargs()
          kwargs['instance'] = Question(event=self.request.event)
          return kwargs
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.questions', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
      def get_object(self, **kwargs):
          return None
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
      @transaction.atomic
      def form_valid(self, form):
          if form.cleaned_data.get('type') in ('M', 'C'):
              if not self.formset.is_valid():
                  return self.get(self.request, *self.args, **self.kwargs)
  
          messages.success(self.request, _('The new question has been created.'))
          ret = super().form_valid(form)
          form.instance.log_action('pretix.event.question.added', user=self.request.user, data=dict(form.cleaned_data))
  
          if form.cleaned_data.get('type') in ('M', 'C'):
              self.save_formset(form.instance)
  
          return ret
  
  
  class QuotaList(PaginationMixin, ListView):
      model = Quota
      context_object_name = 'quotas'
      template_name = 'pretixcontrol/items/quotas.html'
  
      def get_queryset(self):
          qs = self.request.event.quotas.prefetch_related(
              Prefetch(
                  "items",
                  queryset=Item.objects.annotate(
                      has_variations=Exists(ItemVariation.objects.filter(item=OuterRef('pk')))
                  ),
                  to_attr="cached_items"
              ),
              "variations",
              "variations__item",
              Prefetch(
                  "subevent",
                  queryset=self.request.event.subevents.all()
              )
          )
          if self.request.GET.get("subevent", "") != "":
              s = self.request.GET.get("subevent", "")
              qs = qs.filter(subevent_id=s)
  
          valid_orders = {
              '-date': ('-subevent__date_from', 'name'),
              'date': ('subevent__date_from', '-name'),
              'size': ('size', 'name'),
              '-size': ('-size', '-name'),
              'name': ('name',),
              '-name': ('-name',),
          }
  
          if self.request.GET.get("ordering", "-date") in valid_orders:
              qs = qs.order_by(*valid_orders[self.request.GET.get("ordering", "-date")])
  
          return qs
  
      def get_context_data(self, **kwargs):
          ctx = super().get_context_data()
  
          qa = QuotaAvailability()
          qa.queue(*ctx['quotas'])
          qa.compute()
          for quota in ctx['quotas']:
              quota.cached_avail = qa.results[quota]
  
          return ctx
  
  
  class QuotaCreate(EventPermissionRequiredMixin, CreateView):
      model = Quota
      form_class = QuotaForm
      template_name = 'pretixcontrol/items/quota_edit.html'
      permission = 'can_change_items'
      context_object_name = 'quota'
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.quotas', kwargs={
              'organizer': self.request.event.organizer.slug,
              '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.'))
          ret = super().form_valid(form)
          form.instance.log_action('pretix.event.quota.added', user=self.request.user, data=dict(form.cleaned_data))
          return ret
  
      @cached_property
      def copy_from(self):
          if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
              try:
                  return self.request.event.quotas.get(pk=self.request.GET.get("copy_from"))
              except Quota.DoesNotExist:
                  pass
  
      def get_form_kwargs(self):
          kwargs = super().get_form_kwargs()
  
          if self.copy_from:
              i = modelcopy(self.copy_from)
              i.pk = None
              kwargs['instance'] = i
              kwargs.setdefault('initial', {})
              kwargs['initial']['itemvars'] = [str(i.pk) for i in self.copy_from.items.all()] + [
                  '{}-{}'.format(v.item_id, v.pk) for v in self.copy_from.variations.all()
              ]
          else:
              kwargs['instance'] = Quota(event=self.request.event)
          return kwargs
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
  
  class QuotaView(ChartContainingView, DetailView):
      model = Quota
      template_name = 'pretixcontrol/items/quota.html'
      context_object_name = 'quota'
  
      def get_context_data(self, *args, **kwargs):
          ctx = super().get_context_data()
  
          qa = QuotaAvailability(full_results=True)
          qa.queue(self.object)
          qa.compute()
          ctx['avail'] = qa.results[self.object]
  
          data = [
              {
                  'label': gettext('Paid orders'),
                  'value': qa.count_paid_orders[self.object],
                  'sum': True,
              },
              {
                  'label': gettext('Pending orders'),
                  'value': qa.count_pending_orders[self.object],
                  'sum': True,
              },
          ]
          if self.object.release_after_exit:
              data.append({
                  'label': gettext('Exit scans'),
                  'value': -1 * qa.count_exited_orders[self.object],
                  'sum': True,
              })
  
          data += [
              {
                  'label': gettext('Vouchers and waiting list reservations'),
                  'value': qa.count_vouchers[self.object],
                  'sum': True,
              },
              {
                  'label': gettext('Current user\'s carts'),
                  'value': qa.count_cart[self.object],
                  'sum': True,
              },
          ]
  
          sum_values = sum([d['value'] for d in data if d['sum']])
          s = self.object.size - sum_values if self.object.size is not None else gettext('Infinite')
  
          data.append({
              'label': gettext('Available quota'),
              'value': s,
              'sum': False,
              'strong': True
          })
          data.append({
              'label': gettext('Waiting list (pending)'),
              'value': qa.count_waitinglist[self.object],
              'sum': False,
          })
  
          if self.object.size is not None:
              data.append({
                  'label': gettext('Currently for sale'),
                  'value': ctx['avail'][1],
                  'sum': False,
                  'strong': True
              })
  
          for d in data:
              if isinstance(d.get('value', 0), int) and d.get('value', 0) < 0:
                  d['value_abs'] = abs(d['value'])
          ctx['quota_chart_data'] = json.dumps([r for r in data if r.get('sum') and r['value'] >= 0])
          ctx['quota_table_rows'] = list(data)
          ctx['quota_overbooked'] = sum_values - self.object.size if self.object.size is not None else 0
  
          ctx['has_plugins'] = False
          res = (
              Quota.AVAILABILITY_GONE if self.object.size is not None and self.object.size - sum_values <= 0 else
              Quota.AVAILABILITY_OK,
              self.object.size - sum_values if self.object.size is not None else None
          )
          for recv, resp in quota_availability.send(sender=self.request.event, quota=self.object, result=res,
                                                    count_waitinglist=True):
              if resp != res:
                  ctx['has_plugins'] = True
  
          ctx['has_ignore_vouchers'] = Voucher.objects.filter(
              Q(allow_ignore_quota=True) &
              Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now())) &
              Q(
                  (
                      (  # Orders for items which do not have any variations
                          Q(variation__isnull=True) &
                          Q(item_id__in=Quota.items.through.objects.filter(quota_id=self.object.pk).values_list('item_id', flat=True))
                      ) | (  # Orders for items which do have any variations
                          Q(variation__in=Quota.variations.through.objects.filter(quota_id=self.object.pk).values_list(
                              'itemvariation_id', flat=True))
                      )
                  ) | Q(quota=self.object)
              ) &
              Q(redeemed__lt=F('max_usages'))
          ).exists()
          if self.object.closed:
              qa = QuotaAvailability(ignore_closed=True)
              qa.queue(self.object)
              qa.compute()
              ctx['closed_and_sold_out'] = qa.results[self.object][0] <= Quota.AVAILABILITY_ORDERED
  
          return ctx
  
      def get_object(self, queryset=None) -> Quota:
          try:
              return self.request.event.quotas.get(
                  id=self.kwargs['quota']
              )
          except Quota.DoesNotExist:
              raise Http404(_("The requested quota does not exist."))
  
      def post(self, request, *args, **kwargs):
          if not request.user.has_event_permission(request.organizer, request.event, 'can_change_items', request):
              raise PermissionDenied()
          quota = self.get_object()
          if 'reopen' in request.POST:
              quota.closed = False
              quota.save(update_fields=['closed'])
              quota.log_action('pretix.event.quota.opened', user=request.user)
              messages.success(request, _('The quota has been re-opened.'))
          if 'disable' in request.POST:
              quota.closed = False
              quota.close_when_sold_out = False
              quota.save(update_fields=['closed', 'close_when_sold_out'])
              quota.log_action('pretix.event.quota.opened', user=request.user)
              quota.log_action(
                  'pretix.event.quota.changed', user=self.request.user, data={
                      'close_when_sold_out': False
                  }
              )
              messages.success(request, _('The quota has been re-opened and will not close again.'))
          return redirect(reverse('control:event.items.quotas.show', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
              'quota': quota.pk
          }))
  
  
  class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
      model = Quota
      form_class = QuotaForm
      template_name = 'pretixcontrol/items/quota_edit.html'
      permission = 'can_change_items'
      context_object_name = 'quota'
  
      def get_context_data(self, *args, **kwargs):
          ctx = super().get_context_data()
          return ctx
  
      def get_object(self, queryset=None) -> Quota:
          try:
              return self.request.event.quotas.get(
                  id=self.kwargs['quota']
              )
          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
                  }
              )
              if ((form.initial.get('subevent') and not form.instance.subevent) or
                      (form.instance.subevent and form.initial.get('subevent') != form.instance.subevent.pk)):
  
                  if form.initial.get('subevent'):
                      se = SubEvent.objects.get(event=self.request.event, pk=form.initial.get('subevent'))
                      se.log_action(
                          'pretix.subevent.quota.deleted', user=self.request.user, data={
                              'id': form.instance.pk
                          }
                      )
                  if form.instance.subevent:
                      form.instance.subevent.log_action(
                          'pretix.subevent.quota.added', user=self.request.user, data={
                              'id': form.instance.pk
                          }
                      )
              form.instance.rebuild_cache()
          return super().form_valid(form)
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.quotas.show', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
              'quota': self.object.pk
          })
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
  
  class QuotaDelete(EventPermissionRequiredMixin, DeleteView):
      model = Quota
      template_name = 'pretixcontrol/items/quota_delete.html'
      permission = 'can_change_items'
      context_object_name = 'quota'
  
      def get_object(self, queryset=None) -> Quota:
          try:
              return self.request.event.quotas.get(
                  id=self.kwargs['quota']
              )
          except Quota.DoesNotExist:
              raise Http404(_("The requested quota does not exist."))
  
      def get_context_data(self, *args, **kwargs) -> dict:
          context = super().get_context_data(*args, **kwargs)
          context['dependent'] = list(self.object.items.all())
          context['vouchers'] = self.object.vouchers.count()
          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)
  
      def get_success_url(self) -> str:
          return reverse('control:event.items.quotas', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  
  
  class ItemDetailMixin(SingleObjectMixin):
      model = Item
      context_object_name = 'item'
  
      def get_object(self, queryset=None) -> Item:
          try:
              if not hasattr(self, 'object') or not self.object:
                  self.item = self.request.event.items.get(
                      id=self.kwargs['item']
                  )
                  self.object = self.item
              return self.object
          except Item.DoesNotExist:
              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'
      permission = 'can_change_items'
  
      def get_success_url(self) -> str:
          return reverse('control:event.item', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
              'item': self.object.id,
          })
  
      @cached_property
      def copy_from(self):
          if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
              try:
                  return self.request.event.items.get(pk=self.request.GET.get("copy_from"))
              except Item.DoesNotExist:
                  pass
  
      def get_initial(self):
          initial = super().get_initial()
          trs = list(self.request.event.tax_rules.all())
          if len(trs) == 1:
              initial['tax_rule'] = trs[0]
  
          if self.copy_from:
              fields = ('name', 'internal_name', 'category', 'admission', 'default_price', 'tax_rule')
              for f in fields:
                  initial[f] = getattr(self.copy_from, f)
              initial['copy_from'] = self.copy_from
              initial['has_variations'] = self.copy_from.variations.exists()
  
          return initial
  
      @transaction.atomic
      def form_valid(self, form):
          messages.success(self.request, _('Your changes have been saved.'))
  
          ret = super().form_valid(form)
          form.instance.log_action('pretix.event.item.added', user=self.request.user, data={
              k: (form.cleaned_data.get(k).name
                  if isinstance(form.cleaned_data.get(k), File)
                  else form.cleaned_data.get(k))
              for k in form.changed_data
          })
          return ret
  
      def get_form_kwargs(self):
          """
          Returns the keyword arguments for instantiating the form.
          """
          newinst = Item(event=self.request.event)
          kwargs = super().get_form_kwargs()
          kwargs.update({'instance': newinst, 'user': self.request.user})
          return kwargs
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
  
  class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
      form_class = ItemUpdateForm
      template_name = 'pretixcontrol/item/index.html'
      permission = 'can_change_items'
  
      @cached_property
      def plugin_forms(self):
          forms = []
          for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
              if isinstance(resp, (list, tuple)):
                  forms.extend(resp)
              else:
                  forms.append(resp)
          return forms
  
      def get_success_url(self) -> str:
          return reverse('control:event.item', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
              'item': self.get_object().id,
          })
  
      def is_valid(self, form):
          v = (
              form.is_valid()
              and all(f.is_valid() for f in self.plugin_forms)
              and all(f.is_valid() for f in self.formsets.values())
          )
          if v and form.cleaned_data['category'] and form.cleaned_data['category'].is_addon:
              addons = self.formsets['addons'].ordered_forms + [
                  ef for ef in self.formsets['addons'].extra_forms
                  if ef not in self.formsets['addons'].ordered_forms and ef not in self.formsets['addons'].deleted_forms
              ]
              if addons:
                  messages.error(self.request,
                                 _('You cannot add add-ons to a product that is only available as an add-on '
                                   'itself.'))
                  v = False
  
              bundles = [
                  ef for ef in self.formsets['bundles'].forms
                  if ef not in self.formsets['bundles'].deleted_forms
              ]
              if bundles:
                  messages.error(self.request,
                                 _('You cannot add bundles to a product that is only available as an add-on '
                                   'itself.'))
                  v = False
          return v
  
      def post(self, request, *args, **kwargs):
          self.get_object()
          form = self.get_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)
  
      def save_formset(self, key, log_base, attr='item', order=True, serializer=None,
                       rm_verb='removed'):
          for form in self.formsets[key].deleted_forms:
              if not form.instance.pk:
                  continue
              d = {
                  'id': form.instance.pk
              }
              if serializer:
                  d.update(serializer(form.instance, context={'event': self.request.event}).data)
              self.get_object().log_action(
                  'pretix.event.item.{}.{}'.format(log_base, rm_verb), user=self.request.user, data=d
              )
              form.instance.delete()
              form.instance.pk = None
  
          if order:
              forms = self.formsets[key].ordered_forms + [
                  ef for ef in self.formsets[key].extra_forms
                  if ef not in self.formsets[key].ordered_forms and ef not in self.formsets[key].deleted_forms
              ]
          else:
              forms = [
                  ef for ef in self.formsets[key].forms
                  if ef not in self.formsets[key].deleted_forms
              ]
          for i, form in enumerate(forms):
              if order:
                  form.instance.position = i
              setattr(form.instance, attr, self.get_object())
              created = not form.instance.pk
              form.save()
              if form.has_changed() and any(a for a in form.changed_data if a != 'ORDER'):
                  change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
                  if key == 'variations':
                      change_data['value'] = form.instance.value
                  change_data['id'] = form.instance.pk
                  self.get_object().log_action(
                      'pretix.event.item.{}.changed'.format(log_base) if not created else
                      'pretix.event.item.{}.added'.format(log_base),
                      user=self.request.user, data=change_data
                  )
  
      @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 = {
                  k: form.cleaned_data.get(k)
                  for k in form.changed_data
              }
              for f in self.plugin_forms:
                  data.update({
                      k: (f.cleaned_data.get(k).name
                          if isinstance(f.cleaned_data.get(k), File)
                          else f.cleaned_data.get(k))
                      for k in f.changed_data
                  })
              self.object.log_action(
                  'pretix.event.item.changed', user=self.request.user, data=data
              )
              invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
          for f in self.plugin_forms:
              f.save()
  
          for k, v in self.formsets.items():
              if k == 'variations':
                  self.save_formset(
                      'variations', 'variation',
                      serializer=ItemVariationSerializer,
                      rm_verb='deleted'
                  )
              elif k == 'addons':
                  self.save_formset(
                      'addons', 'addons', 'base_item',
                      serializer=ItemAddOnSerializer
                  )
              elif k == 'bundles':
                  self.save_formset(
                      'bundles', 'bundles', 'base_item', order=False,
                      serializer=ItemBundleSerializer
                  )
              else:
                  v.save()
  
          return super().form_valid(form)
  
      def form_invalid(self, form):
          messages.error(self.request, _('We could not save your changes. See below for details.'))
          return super().form_invalid(form)
  
      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:
              messages.info(self.request, _("You disabled this item, but it is still part of a product bundle. "
                                            "Your participants won't be able to buy the bundle unless you remove this "
                                            "item from it."))
  
          return ctx
  
      @cached_property
      def formsets(self):
          f = OrderedDict([
              ('variations', inlineformset_factory(
                  Item, ItemVariation,
                  form=ItemVariationForm, formset=ItemVariationsFormSet,
                  can_order=True, can_delete=True, extra=0
              )(
                  self.request.POST if self.request.method == "POST" else None,
                  queryset=ItemVariation.objects.filter(item=self.get_object()),
                  event=self.request.event, prefix="variations"
              )),
              ('addons', inlineformset_factory(
                  Item, ItemAddOn,
                  form=ItemAddOnForm, formset=ItemAddOnsFormSet,
                  can_order=True, can_delete=True, extra=0
              )(
                  self.request.POST if self.request.method == "POST" else None,
                  queryset=ItemAddOn.objects.filter(base_item=self.get_object()),
                  event=self.request.event, prefix="addons"
              )),
              ('bundles', inlineformset_factory(
                  Item, ItemBundle,
                  form=ItemBundleForm, formset=ItemBundleFormSet,
                  fk_name='base_item',
                  can_order=False, can_delete=True, extra=0
              )(
                  self.request.POST if self.request.method == "POST" else None,
                  queryset=ItemBundle.objects.filter(base_item=self.get_object()),
                  event=self.request.event, item=self.item, prefix="bundles"
              )),
          ])
          if not self.object.has_variations:
              del f['variations']
  
          i = 0
          for rec, resp in item_formsets.send(sender=self.request.event, item=self.item, request=self.request):
              if isinstance(resp, (list, tuple)):
                  for k in resp:
                      f['p-{}'.format(i)] = k
                      i += 1
              else:
                  f['p-{}'.format(i)] = resp
                  i += 1
          return f
  
  
  class ItemDelete(EventPermissionRequiredMixin, DeleteView):
      model = Item
      template_name = 'pretixcontrol/item/delete.html'
      permission = 'can_change_items'
      context_object_name = 'item'
  
      def get_context_data(self, *args, **kwargs) -> dict:
          context = super().get_context_data(*args, **kwargs)
          context['possible'] = self.is_allowed()
          context['vouchers'] = self.object.vouchers.count()
          return context
  
      def is_allowed(self) -> bool:
          return not self.get_object().orderposition_set.exists()
  
      def get_object(self, queryset=None) -> Item:
          if not hasattr(self, 'object') or not self.object:
              try:
                  self.object = self.request.event.items.get(
                      id=self.kwargs['item']
                  )
              except Item.DoesNotExist:
                  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()
          o = self.get_object()
          if o.allow_delete():
              try:
                  CartPosition.objects.filter(addon_to__item=self.get_object()).delete()
                  self.get_object().cartposition_set.all().delete()
                  self.get_object().log_action('pretix.event.item.deleted', user=self.request.user)
                  self.get_object().delete()
              except ProtectedError:
                  o = self.get_object()
                  o.active = False
                  o.save()
                  o.log_action('pretix.event.item.changed', user=self.request.user, data={
                      'active': False
                  })
                  messages.error(self.request, _('The product could not be deleted as some constraints (e.g. data created by '
                                                 'plug-ins) did not allow it. Deleting it could break reporting or other '
                                                 'functionality, so the procut has been disabled instead.'))
              else:
                  messages.success(request, _('The selected product has been deleted.'))
              return HttpResponseRedirect(success_url)
          else:
              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)
  
      def get_success_url(self) -> str:
          return reverse('control:event.items', kwargs={
              'organizer': self.request.event.organizer.slug,
              'event': self.request.event.slug,
          })
  