pricing: add models (#63808)
This commit is contained in:
parent
1ca5e77bd5
commit
f0e8197cd6
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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']
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = []
|
|
@ -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')},
|
||||
),
|
||||
]
|
|
@ -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)
|
|
@ -55,6 +55,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.humanize',
|
||||
'gadjo',
|
||||
'chrono.agendas',
|
||||
'chrono.pricing',
|
||||
'chrono.api',
|
||||
'chrono.manager',
|
||||
'rest_framework',
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue