chrono/chrono/pricing/views.py

644 lines
22 KiB
Python

# chrono - agendas system
# Copyright (C) 2022 Entr'ouvert
#
# 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, either version 3 of the License, or
# (at your option) any later version.
#
# 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 <http://www.gnu.org/licenses/>.
import datetime
import json
from collections import defaultdict
from operator import itemgetter
from django import forms
from django.core.exceptions import PermissionDenied
from django.db.models import Prefetch
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, UpdateView
from chrono.agendas.models import Agenda
from chrono.manager.views import ManagedAgendaMixin, ViewableAgendaMixin
from chrono.pricing.forms import (
AgendaPricingForm,
CriteriaForm,
NewCriteriaForm,
PricingCriteriaCategoryAddForm,
PricingCriteriaCategoryEditForm,
PricingMatrixForm,
PricingVariableFormSet,
)
from chrono.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
class PricingListView(ListView):
template_name = 'chrono/pricing/manager_pricing_list.html'
model = Pricing
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
pricing_list = PricingListView.as_view()
class CriteriaListView(ListView):
template_name = 'chrono/pricing/manager_criteria_list.html'
model = CriteriaCategory
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return CriteriaCategory.objects.prefetch_related('criterias')
criteria_list = CriteriaListView.as_view()
class PricingAddView(CreateView):
template_name = 'chrono/pricing/manager_pricing_form.html'
model = Pricing
fields = ['label']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
pricing_add = PricingAddView.as_view()
class PricingDetailView(DetailView):
template_name = 'chrono/pricing/manager_pricing_detail.html'
model = Pricing
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return (
super()
.get_queryset()
.prefetch_related(
Prefetch(
'categories', queryset=CriteriaCategory.objects.order_by('pricingcriteriacategory__order')
)
)
)
pricing_detail = PricingDetailView.as_view()
class PricingEditView(UpdateView):
template_name = 'chrono/pricing/manager_pricing_form.html'
model = Pricing
fields = ['label', 'slug']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
pricing_edit = PricingEditView.as_view()
class PricingDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Pricing
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-pricing-list')
pricing_delete = PricingDeleteView.as_view()
class PricingExport(DetailView):
model = Pricing
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
attachment = 'attachment; filename="export_pricing_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
response['Content-Disposition'] = attachment
json.dump({'pricing_models': [self.get_object().export_json()]}, response, indent=2)
return response
pricing_export = PricingExport.as_view()
class PricingVariableEdit(FormView):
template_name = 'chrono/pricing/manager_pricing_variable_form.html'
model = Pricing
form_class = PricingVariableFormSet
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def get_initial(self):
return sorted(
({'key': k, 'value': v} for k, v in self.object.extra_variables.items()),
key=itemgetter('key'),
)
def form_valid(self, form):
self.object.extra_variables = {}
for sub_data in form.cleaned_data:
if not sub_data.get('key'):
continue
self.object.extra_variables[sub_data['key']] = sub_data['value']
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
pricing_variable_edit = PricingVariableEdit.as_view()
class PricingCriteriaCategoryAddView(FormView):
template_name = 'chrono/pricing/manager_pricing_criteria_category_form.html'
model = Pricing
form_class = PricingCriteriaCategoryAddForm
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
if self.object.categories.count() >= 3:
raise Http404
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['pricing'] = self.object
return kwargs
def get_context_data(self, **kwargs):
kwargs['object'] = self.object
return super().get_context_data(**kwargs)
def form_valid(self, form):
PricingCriteriaCategory.objects.create(pricing=self.object, category=form.cleaned_data['category'])
return super().form_valid(form)
def get_success_url(self):
return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
pricing_criteria_category_add = PricingCriteriaCategoryAddView.as_view()
class PricingCriteriaCategoryEditView(FormView):
template_name = 'chrono/pricing/manager_pricing_criteria_category_form.html'
model = Pricing
form_class = PricingCriteriaCategoryEditForm
def dispatch(self, request, *args, **kwargs):
self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
self.category = get_object_or_404(self.object.categories, pk=kwargs['category_pk'])
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['pricing'] = self.object
kwargs['category'] = self.category
return kwargs
def get_context_data(self, **kwargs):
kwargs['object'] = self.object
kwargs['category'] = self.category
return super().get_context_data(**kwargs)
def form_valid(self, form):
old_criterias = self.object.criterias.filter(category=self.category)
new_criterias = form.cleaned_data['criterias']
removed_criterias = set(old_criterias) - set(new_criterias)
self.object.criterias.remove(*removed_criterias)
self.object.criterias.add(*new_criterias)
return super().form_valid(form)
def get_success_url(self):
return reverse('chrono-manager-pricing-detail', args=[self.object.pk])
pricing_criteria_category_edit = PricingCriteriaCategoryEditView.as_view()
class PricingCriteriaCategoryDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = CriteriaCategory
pk_url_kwarg = 'category_pk'
def dispatch(self, request, *args, **kwargs):
self.pricing = get_object_or_404(Pricing, pk=kwargs['pk'])
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return self.pricing.categories.all()
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.pricing.categories.remove(self.object)
self.pricing.criterias.remove(*self.pricing.criterias.filter(category=self.object))
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('chrono-manager-pricing-detail', args=[self.pricing.pk])
pricing_criteria_category_delete = PricingCriteriaCategoryDeleteView.as_view()
class PricingCriteriaCategoryOrder(DetailView):
model = Pricing
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
if 'new-order' not in request.GET:
return HttpResponseBadRequest('missing new-order parameter')
pricing = self.get_object()
try:
new_order = [int(x) for x in request.GET['new-order'].split(',')]
except ValueError:
return HttpResponseBadRequest('incorrect new-order parameter')
categories = pricing.categories.all()
if set(new_order) != {x.pk for x in categories} or len(new_order) != len(categories):
return HttpResponseBadRequest('incorrect new-order parameter')
for i, c_id in enumerate(new_order):
PricingCriteriaCategory.objects.filter(pricing=pricing, category=c_id).update(order=i + 1)
return HttpResponse(status=204)
pricing_criteria_category_order = PricingCriteriaCategoryOrder.as_view()
class CriteriaCategoryAddView(CreateView):
template_name = 'chrono/pricing/manager_criteria_category_form.html'
model = CriteriaCategory
fields = ['label']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-pricing-criteria-list')
criteria_category_add = CriteriaCategoryAddView.as_view()
class CriteriaCategoryEditView(UpdateView):
template_name = 'chrono/pricing/manager_criteria_category_form.html'
model = CriteriaCategory
fields = ['label', 'slug']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-pricing-criteria-list')
criteria_category_edit = CriteriaCategoryEditView.as_view()
class CriteriaCategoryDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = CriteriaCategory
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-pricing-criteria-list')
criteria_category_delete = CriteriaCategoryDeleteView.as_view()
class CriteriaCategoryExport(DetailView):
model = CriteriaCategory
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
attachment = 'attachment; filename="export_pricing_category_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
response['Content-Disposition'] = attachment
json.dump({'pricing_categories': [self.get_object().export_json()]}, response, indent=2)
return response
criteria_category_export = CriteriaCategoryExport.as_view()
class CriteriaOrder(DetailView):
model = CriteriaCategory
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
if 'new-order' not in request.GET:
return HttpResponseBadRequest('missing new-order parameter')
category = self.get_object()
try:
new_order = [int(x) for x in request.GET['new-order'].split(',')]
except ValueError:
return HttpResponseBadRequest('incorrect new-order parameter')
criterias = category.criterias.all()
if set(new_order) != {x.pk for x in criterias} or len(new_order) != len(criterias):
return HttpResponseBadRequest('incorrect new-order parameter')
criterias_by_id = {c.pk: c for c in criterias}
for i, c_id in enumerate(new_order):
criterias_by_id[c_id].order = i + 1
criterias_by_id[c_id].save()
return HttpResponse(status=204)
criteria_order = CriteriaOrder.as_view()
class CriteriaAddView(CreateView):
template_name = 'chrono/pricing/manager_criteria_form.html'
model = Criteria
form_class = NewCriteriaForm
def dispatch(self, request, *args, **kwargs):
self.category_pk = kwargs.pop('category_pk')
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].category_id = self.category_pk
return kwargs
def get_success_url(self):
return reverse('chrono-manager-pricing-criteria-list')
criteria_add = CriteriaAddView.as_view()
class CriteriaEditView(UpdateView):
template_name = 'chrono/pricing/manager_criteria_form.html'
model = Criteria
form_class = CriteriaForm
def dispatch(self, request, *args, **kwargs):
self.category_pk = kwargs.pop('category_pk')
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return Criteria.objects.filter(category=self.category_pk)
def get_success_url(self):
return reverse('chrono-manager-pricing-criteria-list')
criteria_edit = CriteriaEditView.as_view()
class CriteriaDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Criteria
def dispatch(self, request, *args, **kwargs):
self.category_pk = kwargs.pop('category_pk')
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return Criteria.objects.filter(category=self.category_pk)
def get_success_url(self):
return reverse('chrono-manager-pricing-criteria-list')
criteria_delete = CriteriaDeleteView.as_view()
class AgendaPricingAddView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/pricing/manager_agenda_pricing_form.html'
model = AgendaPricing
form_class = AgendaPricingForm
def get_success_url(self):
return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
agenda_pricing_add = AgendaPricingAddView.as_view()
class AgendaPricingDetailView(ViewableAgendaMixin, DetailView):
model = AgendaPricing
pk_url_kwarg = 'pricing_pk'
template_name = 'chrono/pricing/manager_agenda_pricing_detail.html'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_queryset(self):
return AgendaPricing.objects.filter(agenda=self.agenda).prefetch_related(
'pricing__criterias__category'
)
def get_context_data(self, **kwargs):
kwargs['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
return super().get_context_data(**kwargs)
agenda_pricing_detail = AgendaPricingDetailView.as_view()
class AgendaPricingEditView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/pricing/manager_agenda_pricing_form.html'
model = AgendaPricing
pk_url_kwarg = 'pricing_pk'
form_class = AgendaPricingForm
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_queryset(self):
return AgendaPricing.objects.filter(agenda=self.agenda)
def get_success_url(self):
return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
agenda_pricing_edit = AgendaPricingEditView.as_view()
class AgendaPricingDeleteView(ManagedAgendaMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = AgendaPricing
pk_url_kwarg = 'pricing_pk'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_queryset(self):
return AgendaPricing.objects.filter(agenda=self.agenda)
agenda_pricing_delete = AgendaPricingDeleteView.as_view()
class AgendaPricingMatrixEdit(ManagedAgendaMixin, FormView):
template_name = 'chrono/pricing/manager_agenda_pricing_matrix_form.html'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
self.object = get_object_or_404(
AgendaPricing.objects.filter(agenda=self.agenda), pk=kwargs['pricing_pk']
)
matrix_list = list(self.object.iter_pricing_matrix())
if not matrix_list:
raise Http404
self.matrix = None
if kwargs.get('slug'):
for matrix in matrix_list:
if matrix.criteria is None:
continue
if matrix.criteria.slug == kwargs['slug']:
self.matrix = matrix
break
else:
if matrix_list[0].criteria is None:
self.matrix = matrix_list[0]
if self.matrix is None:
raise Http404
def get_context_data(self, **kwargs):
kwargs['object'] = self.object
kwargs['matrix'] = self.matrix
return super().get_context_data(**kwargs)
def get_form(self):
count = len(self.matrix.rows)
PricingMatrixFormSet = forms.formset_factory(
PricingMatrixForm, min_num=count, max_num=count, extra=0, can_delete=False
)
kwargs = {
'initial': [
{'crit_%i' % i: cell.value for i, cell in enumerate(row.cells)} for row in self.matrix.rows
]
}
if self.request.method == 'POST':
kwargs.update(
{
'data': self.request.POST,
}
)
return PricingMatrixFormSet(form_kwargs={'matrix': self.matrix}, **kwargs)
def post(self, *args, **kwargs):
form = self.get_form()
if form.is_valid():
# build prixing_data for this matrix
matrix_pricing_data = defaultdict(dict)
for i, sub_data in enumerate(form.cleaned_data):
row = self.matrix.rows[i]
for j, cell in enumerate(row.cells):
value = sub_data['crit_%s' % j]
key = cell.criteria.identifier if cell.criteria else None
matrix_pricing_data[key][row.criteria.identifier] = float(value)
if self.matrix.criteria:
# full pricing model with 3 categories
self.object.pricing_data = self.object.pricing_data or {}
self.object.pricing_data[self.matrix.criteria.identifier] = matrix_pricing_data
elif list(matrix_pricing_data.keys()) == [None]:
# only one category
self.object.pricing_data = matrix_pricing_data[None]
else:
# 2 categories
self.object.pricing_data = matrix_pricing_data
self.object.save()
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse('chrono-manager-agenda-pricing-detail', args=[self.agenda.pk, self.object.pk])
agenda_pricing_matrix_edit = AgendaPricingMatrixEdit.as_view()