pricing: duplicate pricing model (#65231)

This commit is contained in:
Lauréline Guérin 2022-05-13 16:57:10 +02:00
parent fe16e1af83
commit 272c541b94
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
8 changed files with 163 additions and 0 deletions

View File

@ -51,6 +51,10 @@ class CriteriaForm(NewCriteriaForm):
return slug
class PricingDuplicateForm(forms.Form):
label = forms.CharField(label=_('New label'), max_length=150, required=False)
class PricingVariableForm(forms.Form):
key = forms.CharField(label=_('Variable name'), required=False)
value = forms.CharField(

View File

@ -14,6 +14,7 @@
# 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 copy
import dataclasses
import decimal
@ -252,6 +253,24 @@ class Pricing(models.Model):
'categories': [pcc.export_json() for pcc in PricingCriteriaCategory.objects.filter(pricing=self)],
}
def duplicate(self, label=None):
# clone current pricing
new_pricing = copy.deepcopy(self)
new_pricing.pk = None
new_pricing.label = label or _('Copy of %s') % self.label
# reset slug
new_pricing.slug = None
new_pricing.save()
# set criterias
new_pricing.criterias.set(self.criterias.all())
# set categories
for pcc in PricingCriteriaCategory.objects.filter(pricing=self):
pcc.duplicate(pricing_target=new_pricing)
return new_pricing
class PricingCriteriaCategory(models.Model):
pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
@ -280,6 +299,13 @@ class PricingCriteriaCategory(models.Model):
'criterias': [c.slug for c in self.pricing.criterias.all() if c.category == self.category],
}
def duplicate(self, pricing_target):
new_pcc = copy.deepcopy(self)
new_pcc.pk = None
new_pcc.pricing = pricing_target
new_pcc.save()
return new_pcc
@dataclasses.dataclass
class PricingMatrixCell:

View File

@ -15,6 +15,7 @@
<a class="extra-actions-menu-opener"></a>
<ul class="extra-actions-menu">
<li><a rel="popup" href="{% url 'chrono-manager-pricing-edit' pk=object.pk %}">{% trans 'Options' %}</a></li>
<li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-pricing-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
<li><a href="{% url 'chrono-manager-pricing-export' pk=object.pk %}">{% trans 'Export' %}</a></li>
<li><a rel="popup" href="{% url 'chrono-manager-pricing-delete' pk=object.pk %}">{% trans 'Delete' %}</a></li>
</ul>

View File

@ -0,0 +1,22 @@
{% extends "chrono/pricing/manager_pricing_detail.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-pricing-duplicate' object.pk %}">{% trans "Duplicate pricing" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Duplicate pricing" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Duplicate" %}</button>
<a class="cancel" href="{% url 'chrono-manager-pricing-detail' object.pk %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -40,6 +40,11 @@ urlpatterns = [
views.pricing_delete,
name='chrono-manager-pricing-delete',
),
url(
r'^(?P<pk>\d+)/duplicate/$',
views.pricing_duplicate,
name='chrono-manager-pricing-duplicate',
),
url(
r'^(?P<pk>\d+)/export/$',
views.pricing_export,

View File

@ -26,6 +26,7 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpRespo
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 django.views.generic.detail import SingleObjectMixin
from chrono.agendas.models import Agenda
from chrono.manager.views import ManagedAgendaMixin, ViewableAgendaMixin
@ -35,6 +36,7 @@ from chrono.pricing.forms import (
NewCriteriaForm,
PricingCriteriaCategoryAddForm,
PricingCriteriaCategoryEditForm,
PricingDuplicateForm,
PricingMatrixForm,
PricingVariableFormSet,
)
@ -144,6 +146,28 @@ class PricingDeleteView(DeleteView):
pricing_delete = PricingDeleteView.as_view()
class PricingDuplicate(SingleObjectMixin, FormView):
template_name = 'chrono/pricing/manager_pricing_duplicate_form.html'
model = Pricing
form_class = PricingDuplicateForm
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
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', kwargs={'pk': self.new_pricing.pk})
def form_valid(self, form):
self.new_pricing = self.object.duplicate(label=form.cleaned_data['label'])
return super().form_valid(form)
pricing_duplicate = PricingDuplicate.as_view()
class PricingExport(DetailView):
model = Pricing

View File

@ -112,6 +112,36 @@ def test_delete_pricing_as_manager(app, manager_user):
app.get('/manage/pricing/%s/delete/' % pricing.pk, status=403)
def test_duplicate_pricing(app, admin_user):
pricing = Pricing.objects.create(label='Model')
assert Pricing.objects.count() == 1
app = login(app)
resp = app.get('/manage/pricing/%s/' % pricing.pk)
resp = resp.click(href='/manage/pricing/%s/duplicate/' % pricing.pk)
resp = resp.form.submit()
assert Pricing.objects.count() == 2
new_pricing = Pricing.objects.latest('pk')
assert resp.location == '/manage/pricing/%s/' % new_pricing.pk
assert new_pricing.pk != pricing.pk
resp = resp.follow()
assert 'copy-of-model' in resp.text
resp = resp.click(href='/manage/pricing/%s/duplicate/' % new_pricing.pk)
resp.form['label'] = 'hop'
resp = resp.form.submit().follow()
assert 'hop' in resp.text
def test_duplicate_pricing_as_manager(app, manager_user):
pricing = Pricing.objects.create(label='Model')
app = login(app, username='manager', password='manager')
app.get('/manage/pricing/%s/duplicate/' % pricing.pk, status=403)
@pytest.mark.freeze_time('2021-07-08')
def test_import_pricing(app, admin_user):
pricing = Pricing.objects.create(label='Model')

View File

@ -171,6 +171,57 @@ def test_pricing_category_criteria_duplicate_orders():
assert pcc.order == 3
def test_pricing_duplicate():
category1 = CriteriaCategory.objects.create(label='Cat 1')
Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
Criteria.objects.create(label='Crit 1-2', slug='crit-1-2', category=category1, order=2)
category2 = CriteriaCategory.objects.create(label='Cat 2')
Criteria.objects.create(label='Crit 2-1', slug='crit-2-1', category=category2, order=1)
Criteria.objects.create(label='Crit 2-2', slug='crit-2-2', category=category2, order=2)
Criteria.objects.create(label='Crit 2-3', slug='crit-2-3', category=category2, order=3)
category3 = CriteriaCategory.objects.create(label='Cat 3')
Criteria.objects.create(label='Crit 3-1', slug='crit-3-1', category=category3, order=1)
Criteria.objects.create(label='Crit 3-3', slug='crit-3-3', category=category3, order=3)
Criteria.objects.create(label='Crit 3-4', slug='crit-3-4', category=category3, order=4)
Criteria.objects.create(label='Crit 3-2', slug='crit-3-2', category=category3, order=2)
not_used = Criteria.objects.create(label='Not used', slug='crit-3-notused', category=category3, order=5)
pricing = Pricing.objects.create(
label='Foo',
extra_variables={
'foo': 'bar',
},
)
pricing.categories.add(category1, through_defaults={'order': 1})
pricing.categories.add(category2, through_defaults={'order': 2})
pricing.categories.add(category3, through_defaults={'order': 3})
pricing.criterias.set(Criteria.objects.exclude(pk=not_used.pk))
new_pricing = pricing.duplicate()
assert new_pricing.label == 'Copy of Foo'
assert new_pricing.slug == 'copy-of-foo'
assert new_pricing.extra_variables == pricing.extra_variables
assert list(new_pricing.criterias.all()) == list(pricing.criterias.all())
original_pcc_categories = list(
PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
)
new_pcc_categories = list(
PricingCriteriaCategory.objects.filter(pricing=new_pricing).values_list('category', flat=True)
)
assert new_pcc_categories == original_pcc_categories
original_pcc_orders = list(
PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)
)
new_pcc_orders = list(
PricingCriteriaCategory.objects.filter(pricing=new_pricing).values_list('order', flat=True)
)
assert new_pcc_orders == original_pcc_orders
new_pricing = pricing.duplicate(label='Bar')
assert new_pricing.label == 'Bar'
assert new_pricing.slug == 'bar'
def test_get_agenda_pricing():
agenda = Agenda.objects.create(label='Foo bar', kind='events')
pricing = Pricing.objects.create(label='Foo bar')