pricing: add models (#63808)

This commit is contained in:
Lauréline Guérin 2022-04-25 15:49:42 +02:00
parent 1ca5e77bd5
commit f0e8197cd6
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
10 changed files with 461 additions and 25 deletions

View File

@ -0,0 +1,19 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pricing', '0002_pricing'),
('agendas', '0125_auto_20220502_1134'),
]
operations = [
migrations.AddField(
model_name='agenda',
name='pricings',
field=models.ManyToManyField(
related_name='agendas', through='pricing.AgendaPricing', to='pricing.Pricing'
),
),
]

View File

@ -56,6 +56,7 @@ from django.utils.translation import ungettext
from chrono.interval import Interval, IntervalSet
from chrono.utils.date import get_weekday_index
from chrono.utils.db import ArraySubquery, SumCardinality
from chrono.utils.misc import generate_slug
from chrono.utils.publik_urls import translate_from_publik_url
from chrono.utils.requests_wrapper import requests as requests_wrapper
@ -97,31 +98,6 @@ def is_midnight(dtime):
return dtime.hour == 0 and dtime.minute == 0
def generate_slug(instance, seen_slugs=None, **query_filters):
base_slug = instance.base_slug
slug = base_slug
i = 1
if seen_slugs is None:
# no optimization: check slug in DB each time
while instance._meta.model.objects.filter(slug=slug, **query_filters).exists():
slug = '%s-%s' % (base_slug, i)
i += 1
return slug
# seen_slugs is filled
while True:
if slug not in seen_slugs:
# check in DB to be sure, but only if not seen
queryset = instance._meta.model.objects.filter(slug=slug, **query_filters)
if not queryset.exists():
break
slug = '%s-%s' % (base_slug, i)
i += 1
seen_slugs.add(slug)
return slug
def clean_import_data(cls, data):
cleaned_data = copy.deepcopy(data)
for param in data:
@ -305,6 +281,11 @@ class Agenda(models.Model):
null=True,
blank=True,
)
pricings = models.ManyToManyField(
'pricing.Pricing',
related_name='agendas',
through='pricing.AgendaPricing',
)
class Meta:
ordering = ['label']

View File

View File

@ -0,0 +1,10 @@
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = []

View File

@ -0,0 +1,132 @@
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('agendas', '0124_check_type_disabled'),
('pricing', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Criteria',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('label', models.CharField(max_length=150, verbose_name='Label')),
('slug', models.SlugField(max_length=160, verbose_name='Identifier')),
('condition', models.CharField(max_length=1000, verbose_name='Condition')),
('order', models.PositiveIntegerField()),
],
options={
'ordering': ['order'],
},
),
migrations.CreateModel(
name='CriteriaCategory',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('label', models.CharField(max_length=150, verbose_name='Label')),
('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
],
options={
'ordering': ['label'],
},
),
migrations.CreateModel(
name='Pricing',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('label', models.CharField(max_length=150, verbose_name='Label')),
('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
],
options={
'ordering': ['label'],
},
),
migrations.CreateModel(
name='PricingCriteriaCategory',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('order', models.PositiveIntegerField()),
(
'category',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='pricing.CriteriaCategory'
),
),
(
'pricing',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.Pricing'),
),
],
options={
'ordering': ['order'],
'unique_together': {('pricing', 'category')},
},
),
migrations.AddField(
model_name='pricing',
name='categories',
field=models.ManyToManyField(
related_name='pricings',
through='pricing.PricingCriteriaCategory',
to='pricing.CriteriaCategory',
),
),
migrations.AddField(
model_name='pricing',
name='criterias',
field=models.ManyToManyField(to='pricing.Criteria'),
),
migrations.AddField(
model_name='criteria',
name='category',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='criterias',
to='pricing.CriteriaCategory',
verbose_name='Category',
),
),
migrations.CreateModel(
name='AgendaPricing',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('date_start', models.DateField()),
('date_end', models.DateField()),
('pricing_data', django.contrib.postgres.fields.jsonb.JSONField(null=True)),
(
'agenda',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agendas.Agenda'),
),
(
'pricing',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pricing.Pricing'),
),
],
),
migrations.AlterUniqueTogether(
name='criteria',
unique_together={('category', 'slug')},
),
]

View File

131
chrono/pricing/models.py Normal file
View File

@ -0,0 +1,131 @@
# 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/>.
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from chrono.utils.misc import generate_slug
class CriteriaCategory(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
class Meta:
ordering = ['label']
def __str__(self):
return self.label
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug(self)
super().save(*args, **kwargs)
@property
def base_slug(self):
return slugify(self.label)
class Criteria(models.Model):
category = models.ForeignKey(
CriteriaCategory, verbose_name=_('Category'), on_delete=models.CASCADE, related_name='criterias'
)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160)
condition = models.CharField(_('Condition'), max_length=1000)
order = models.PositiveIntegerField()
class Meta:
ordering = ['order']
unique_together = ['category', 'slug']
def __str__(self):
return self.label
def save(self, *args, **kwargs):
if self.order is None:
max_order = (
Criteria.objects.filter(category=self.category)
.aggregate(models.Max('order'))
.get('order__max')
or 0
)
self.order = max_order + 1
if not self.slug:
self.slug = generate_slug(self, category=self.category)
super().save(*args, **kwargs)
@property
def base_slug(self):
return slugify(self.label)
class Pricing(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
categories = models.ManyToManyField(
CriteriaCategory,
related_name='pricings',
through='PricingCriteriaCategory',
)
criterias = models.ManyToManyField(Criteria)
class Meta:
ordering = ['label']
def __str__(self):
return self.label
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug(self)
super().save(*args, **kwargs)
@property
def base_slug(self):
return slugify(self.label)
class PricingCriteriaCategory(models.Model):
pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
category = models.ForeignKey(CriteriaCategory, on_delete=models.CASCADE)
order = models.PositiveIntegerField()
class Meta:
ordering = ['order']
unique_together = ['pricing', 'category']
def save(self, *args, **kwargs):
if self.order is None:
max_order = (
PricingCriteriaCategory.objects.filter(pricing=self.pricing)
.aggregate(models.Max('order'))
.get('order__max')
or 0
)
self.order = max_order + 1
super().save(*args, **kwargs)
class AgendaPricing(models.Model):
agenda = models.ForeignKey('agendas.Agenda', on_delete=models.CASCADE)
pricing = models.ForeignKey(Pricing, on_delete=models.CASCADE)
date_start = models.DateField()
date_end = models.DateField()
pricing_data = JSONField(null=True)

View File

@ -55,6 +55,7 @@ INSTALLED_APPS = (
'django.contrib.humanize',
'gadjo',
'chrono.agendas',
'chrono.pricing',
'chrono.api',
'chrono.manager',
'rest_framework',

40
chrono/utils/misc.py Normal file
View File

@ -0,0 +1,40 @@
# 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/>.
def generate_slug(instance, seen_slugs=None, **query_filters):
base_slug = instance.base_slug
slug = base_slug
i = 1
if seen_slugs is None:
# no optimization: check slug in DB each time
while instance._meta.model.objects.filter(slug=slug, **query_filters).exists():
slug = '%s-%s' % (base_slug, i)
i += 1
return slug
# seen_slugs is filled
while True:
if slug not in seen_slugs:
# check in DB to be sure, but only if not seen
queryset = instance._meta.model.objects.filter(slug=slug, **query_filters)
if not queryset.exists():
break
slug = '%s-%s' % (base_slug, i)
i += 1
seen_slugs.add(slug)
return slug

122
tests/test_pricing.py Normal file
View File

@ -0,0 +1,122 @@
import pytest
from chrono.pricing.models import Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
pytestmark = pytest.mark.django_db
def test_criteria_category_slug():
category = CriteriaCategory.objects.create(label='Foo bar')
assert category.slug == 'foo-bar'
def test_criteria_category_existing_slug():
category = CriteriaCategory.objects.create(label='Foo bar', slug='bar')
assert category.slug == 'bar'
def test_criteria_category_duplicate_slugs():
category = CriteriaCategory.objects.create(label='Foo baz')
assert category.slug == 'foo-baz'
category = CriteriaCategory.objects.create(label='Foo baz')
assert category.slug == 'foo-baz-1'
category = CriteriaCategory.objects.create(label='Foo baz')
assert category.slug == 'foo-baz-2'
def test_criteria_slug():
category = CriteriaCategory.objects.create(label='Foo')
criteria = Criteria.objects.create(label='Foo bar', category=category)
assert criteria.slug == 'foo-bar'
def test_criteria_existing_slug():
category = CriteriaCategory.objects.create(label='Foo')
criteria = Criteria.objects.create(label='Foo bar', slug='bar', category=category)
assert criteria.slug == 'bar'
def test_criteria_duplicate_slugs():
category = CriteriaCategory.objects.create(label='Foo')
category2 = CriteriaCategory.objects.create(label='Bar')
Criteria.objects.create(label='Foo baz', slug='foo-baz', category=category2)
criteria = Criteria.objects.create(label='Foo baz', category=category)
assert criteria.slug == 'foo-baz'
criteria = Criteria.objects.create(label='Foo baz', category=category)
assert criteria.slug == 'foo-baz-1'
criteria = Criteria.objects.create(label='Foo baz', category=category)
assert criteria.slug == 'foo-baz-2'
def test_criteria_order():
category = CriteriaCategory.objects.create(label='Foo')
criteria = Criteria.objects.create(label='Foo bar', category=category)
assert criteria.order == 1
def test_criteria_existing_order():
category = CriteriaCategory.objects.create(label='Foo')
criteria = Criteria.objects.create(label='Foo bar', order=42, category=category)
assert criteria.order == 42
def test_criteria_duplicate_orders():
category = CriteriaCategory.objects.create(label='Foo')
category2 = CriteriaCategory.objects.create(label='Bar')
Criteria.objects.create(label='Foo baz', order=1, category=category2)
criteria = Criteria.objects.create(label='Foo baz', category=category)
assert criteria.order == 1
criteria = Criteria.objects.create(label='Foo baz', category=category)
assert criteria.order == 2
criteria = Criteria.objects.create(label='Foo baz', category=category)
assert criteria.order == 3
def test_pricing_slug():
pricing = Pricing.objects.create(label='Foo bar')
assert pricing.slug == 'foo-bar'
def test_pricing_existing_slug():
pricing = Pricing.objects.create(label='Foo bar', slug='bar')
assert pricing.slug == 'bar'
def test_pricing_duplicate_slugs():
pricing = Pricing.objects.create(label='Foo baz')
assert pricing.slug == 'foo-baz'
pricing = Pricing.objects.create(label='Foo baz')
assert pricing.slug == 'foo-baz-1'
pricing = Pricing.objects.create(label='Foo baz')
assert pricing.slug == 'foo-baz-2'
def test_pricing_category_criteria_order():
category = CriteriaCategory.objects.create(label='Foo')
pricing = Pricing.objects.create(label='Foo bar')
pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category)
assert pcc.order == 1
def test_pricing_category_criteria_existing_order():
category = CriteriaCategory.objects.create(label='Foo')
pricing = Pricing.objects.create(label='Foo bar')
pcc = PricingCriteriaCategory.objects.create(order=42, pricing=pricing, category=category)
assert pcc.order == 42
def test_pricing_category_criteria_duplicate_orders():
category1 = CriteriaCategory.objects.create(label='Foo')
category2 = CriteriaCategory.objects.create(label='Bar')
category3 = CriteriaCategory.objects.create(label='Baz')
pricing = Pricing.objects.create(label='Foo bar')
pricing2 = Pricing.objects.create(label='Foo baz')
PricingCriteriaCategory.objects.create(order=1, pricing=pricing2, category=category1)
PricingCriteriaCategory.objects.create(order=2, pricing=pricing2, category=category2)
PricingCriteriaCategory.objects.create(order=3, pricing=pricing2, category=category3)
pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category1)
assert pcc.order == 1
pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category2)
assert pcc.order == 2
pcc = PricingCriteriaCategory.objects.create(pricing=pricing, category=category3)
assert pcc.order == 3