Projet

Général

Profil

0001-pricing-import-export-65442.patch

Lauréline Guérin, 20 mai 2022 16:36

Télécharger (42,2 ko)

Voir les différences:

Subject: [PATCH 1/3] pricing: import/export (#65442)

 lingo/agendas/models.py                       |  32 +-
 lingo/pricing/forms.py                        |  12 +
 lingo/pricing/management/__init__.py          |   0
 lingo/pricing/management/commands/__init__.py |   0
 .../management/commands/export_site.py        |  39 ++
 .../management/commands/import_site.py        |  54 +++
 .../templates/lingo/pricing/export.html       |  22 +
 .../templates/lingo/pricing/import.html       |  22 +
 .../lingo/pricing/manager_pricing_list.html   |   9 +-
 lingo/pricing/urls.py                         |   2 +
 lingo/pricing/utils.py                        |  75 ++++
 lingo/pricing/views.py                        | 144 ++++++-
 tests/pricing/test_import_export.py           | 375 ++++++++++++++++++
 tests/pricing/test_manager.py                 |  78 ++--
 14 files changed, 836 insertions(+), 28 deletions(-)
 create mode 100644 lingo/pricing/management/__init__.py
 create mode 100644 lingo/pricing/management/commands/__init__.py
 create mode 100644 lingo/pricing/management/commands/export_site.py
 create mode 100644 lingo/pricing/management/commands/import_site.py
 create mode 100644 lingo/pricing/templates/lingo/pricing/export.html
 create mode 100644 lingo/pricing/templates/lingo/pricing/import.html
 create mode 100644 lingo/pricing/utils.py
 create mode 100644 tests/pricing/test_import_export.py
lingo/agendas/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import copy
18

  
17 19
from django.db import models
18 20
from django.utils.text import slugify
19 21
from django.utils.translation import ugettext_lazy as _
20 22

  
21
from lingo.utils.misc import clean_import_data, generate_slug
23
from lingo.utils.misc import AgendaImportError, clean_import_data, generate_slug
22 24

  
23 25

  
24 26
class Agenda(models.Model):
......
37 39
    def base_slug(self):
38 40
        return slugify(self.label)
39 41

  
42
    def export_json(self):
43
        return {
44
            'slug': self.slug,
45
            'pricings': [x.export_json() for x in self.agendapricing_set.all()],
46
        }
47

  
48
    @classmethod
49
    def import_json(cls, data, overwrite=False):
50
        from lingo.pricing.models import AgendaPricing, Pricing
51

  
52
        data = copy.deepcopy(data)
53
        try:
54
            agenda = Agenda.objects.get(slug=data['slug'])
55
        except Agenda.DoesNotExist:
56
            raise AgendaImportError(_('Missing "%s" agenda') % data['slug'])
57
        pricings = data.pop('pricings', None) or []
58
        for pricing_data in pricings:
59
            try:
60
                pricing_data['pricing'] = Pricing.objects.get(slug=pricing_data['pricing'])
61
            except Pricing.DoesNotExist:
62
                raise AgendaImportError(_('Missing "%s" pricing model') % pricing_data['pricing'])
63

  
64
        for pricing_data in pricings:
65
            pricing_data['agenda'] = agenda
66
            AgendaPricing.import_json(pricing_data)
67

  
68
        return agenda, False
69

  
40 70
    def can_be_managed(self, user):
41 71
        return True
42 72

  
lingo/pricing/forms.py
22 22
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory
23 23

  
24 24

  
25
class ExportForm(forms.Form):
26
    agendas = forms.BooleanField(label=_('Agendas'), required=False, initial=True)
27
    pricing_categories = forms.BooleanField(
28
        label=_('Pricing criteria categories'), required=False, initial=True
29
    )
30
    pricing_models = forms.BooleanField(label=_('Pricing models'), required=False, initial=True)
31

  
32

  
33
class ImportForm(forms.Form):
34
    config_json = forms.FileField(label=_('Export File'))
35

  
36

  
25 37
class NewCriteriaForm(forms.ModelForm):
26 38
    class Meta:
27 39
        model = Criteria
lingo/pricing/management/commands/export_site.py
1
# lingo - payment and billing system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18
import sys
19

  
20
from django.core.management.base import BaseCommand
21

  
22
from lingo.pricing.utils import export_site
23

  
24

  
25
class Command(BaseCommand):
26
    help = 'Export the site'
27

  
28
    def add_arguments(self, parser):
29
        parser.add_argument(
30
            '--output', metavar='FILE', default=None, help='name of a file to write output to'
31
        )
32

  
33
    def handle(self, *args, **options):
34
        if options['output']:
35
            with open(options['output'], 'w') as output:
36
                json.dump(export_site(), output, indent=4)
37
        else:
38
            output = sys.stdout
39
            json.dump(export_site(), output, indent=4)
lingo/pricing/management/commands/import_site.py
1
# lingo - payment and billing system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18
import sys
19

  
20
from django.core.management.base import BaseCommand, CommandError
21

  
22
from lingo.pricing.utils import import_site
23
from lingo.utils.misc import AgendaImportError
24

  
25

  
26
class Command(BaseCommand):
27
    help = 'Import an exported site'
28

  
29
    def add_arguments(self, parser):
30
        parser.add_argument('filename', metavar='FILENAME', type=str, help='name of file to import')
31
        parser.add_argument('--clean', action='store_true', default=False, help='Clean site before importing')
32
        parser.add_argument(
33
            '--if-empty', action='store_true', default=False, help='Import only if site is empty'
34
        )
35
        parser.add_argument('--overwrite', action='store_true', default=False, help='Overwrite existing data')
36

  
37
    def handle(self, filename, **options):
38
        def do_import(fd):
39
            try:
40
                import_site(
41
                    json.load(fd),
42
                    if_empty=options['if_empty'],
43
                    clean=options['clean'],
44
                    overwrite=options['overwrite'],
45
                )
46
            except AgendaImportError as exc:
47
                raise CommandError('%s' % exc)
48

  
49
        if filename == '-':
50
            fd = sys.stdin
51
            do_import(fd)
52
        else:
53
            with open(filename) as fd:
54
                do_import(fd)
lingo/pricing/templates/lingo/pricing/export.html
1
{% extends "lingo/pricing/manager_pricing_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'lingo-manager-config-export' %}">{% trans 'Export' %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Export" %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.as_p }}
17
  <div class="buttons">
18
    <button class="submit-button">{% trans "Export" %}</button>
19
    <a class="cancel" href="{% url 'lingo-manager-pricing-list' %}">{% trans 'Cancel' %}</a>
20
  </div>
21
</form>
22
{% endblock %}
lingo/pricing/templates/lingo/pricing/import.html
1
{% extends "lingo/pricing/manager_pricing_list.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'lingo-manager-config-import' %}">{% trans 'Import' %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Import" %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.as_p }}
17
  <div class="buttons">
18
    <button class="submit-button">{% trans "Import" %}</button>
19
    <a class="cancel" href="{% url 'lingo-manager-pricing-list' %}">{% trans 'Cancel' %}</a>
20
  </div>
21
</form>
22
{% endblock %}
lingo/pricing/templates/lingo/pricing/manager_pricing_list.html
9 9
{% block appbar %}
10 10
<h2>{% trans 'Pricing' context 'pricing' %}</h2>
11 11
<span class="actions">
12
<a href="{% url 'lingo-manager-pricing-criteria-list' %}">{% trans 'Criterias' %}</a>
13
<a rel="popup" href="{% url 'lingo-manager-pricing-add' %}">{% trans 'New pricing model' %}</a>
12
  <a class="extra-actions-menu-opener"></a>
13
  <ul class="extra-actions-menu">
14
    <li><a rel="popup" href="{% url 'lingo-manager-config-import' %}">{% trans 'Import' %}</a></li>
15
    <li><a rel="popup" href="{% url 'lingo-manager-config-export' %}" data-autoclose-dialog="true">{% trans 'Export' %}</a></li>
16
  </ul>
17
  <a href="{% url 'lingo-manager-pricing-criteria-list' %}">{% trans 'Criterias' %}</a>
18
  <a rel="popup" href="{% url 'lingo-manager-pricing-add' %}">{% trans 'New pricing model' %}</a>
14 19
</span>
15 20
{% endblock %}
16 21

  
lingo/pricing/urls.py
20 20

  
21 21
urlpatterns = [
22 22
    url(r'^$', views.pricing_list, name='lingo-manager-pricing-list'),
23
    url(r'^import/$', views.config_import, name='lingo-manager-config-import'),
24
    url(r'^export/$', views.config_export, name='lingo-manager-config-export'),
23 25
    url(
24 26
        r'^add/$',
25 27
        views.pricing_add,
lingo/pricing/utils.py
1
# lingo - payment and billing system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import collections
18

  
19
from django.db import transaction
20

  
21
from lingo.agendas.models import Agenda
22
from lingo.pricing.models import AgendaPricing, CriteriaCategory, Pricing
23

  
24

  
25
def export_site(
26
    agendas=True,
27
    pricing_categories=True,
28
    pricing_models=True,
29
):
30
    '''Dump site objects to JSON-dumpable dictionnary'''
31
    data = collections.OrderedDict()
32
    if pricing_models:
33
        data['pricing_models'] = [x.export_json() for x in Pricing.objects.all()]
34
    if pricing_categories:
35
        data['pricing_categories'] = [x.export_json() for x in CriteriaCategory.objects.all()]
36
    if agendas:
37
        data['agendas'] = [x.export_json() for x in Agenda.objects.all()]
38
    return data
39

  
40

  
41
def import_site(data, if_empty=False, clean=False, overwrite=False):
42
    if if_empty and (
43
        AgendaPricing.objects.exists() or CriteriaCategory.objects.exists() or Pricing.objects.exists()
44
    ):
45
        return
46

  
47
    if clean:
48
        AgendaPricing.objects.all().delete()
49
        CriteriaCategory.objects.all().delete()
50
        Pricing.objects.all().delete()
51

  
52
    results = {
53
        key: collections.defaultdict(list)
54
        for key in [
55
            'agendas',
56
            'pricing_categories',
57
            'pricing_models',
58
        ]
59
    }
60

  
61
    with transaction.atomic():
62
        for cls, key in (
63
            (CriteriaCategory, 'pricing_categories'),
64
            (Pricing, 'pricing_models'),
65
            (Agenda, 'agendas'),
66
        ):
67
            objs = data.get(key, [])
68
            for obj in objs:
69
                created, obj = cls.import_json(obj, overwrite=overwrite)
70
                results[key]['all'].append(obj)
71
                if created:
72
                    results[key]['created'].append(obj)
73
                else:
74
                    results[key]['updated'].append(obj)
75
    return results
lingo/pricing/views.py
20 20
from operator import itemgetter
21 21

  
22 22
from django import forms
23
from django.contrib import messages
23 24
from django.core.exceptions import PermissionDenied
24 25
from django.db.models import Prefetch
25 26
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
26 27
from django.shortcuts import get_object_or_404
27
from django.urls import reverse
28
from django.urls import reverse, reverse_lazy
29
from django.utils.encoding import force_text
30
from django.utils.translation import ugettext_lazy as _
31
from django.utils.translation import ungettext
28 32
from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, UpdateView
29 33
from django.views.generic.detail import SingleObjectMixin
30 34

  
......
33 37
from lingo.pricing.forms import (
34 38
    AgendaPricingForm,
35 39
    CriteriaForm,
40
    ExportForm,
41
    ImportForm,
36 42
    NewCriteriaForm,
37 43
    PricingCriteriaCategoryAddForm,
38 44
    PricingCriteriaCategoryEditForm,
......
41 47
    PricingVariableFormSet,
42 48
)
43 49
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
50
from lingo.pricing.utils import export_site, import_site
51
from lingo.utils.misc import AgendaImportError
52

  
53

  
54
class ConfigExportView(FormView):
55
    form_class = ExportForm
56
    template_name = 'lingo/pricing/export.html'
57

  
58
    def dispatch(self, request, *args, **kwargs):
59
        if not request.user.is_staff:
60
            raise PermissionDenied()
61
        return super().dispatch(request, *args, **kwargs)
62

  
63
    def form_valid(self, form):
64
        response = HttpResponse(content_type='application/json')
65
        today = datetime.date.today()
66
        response['Content-Disposition'] = 'attachment; filename="export_pricing_config_{}.json"'.format(
67
            today.strftime('%Y%m%d')
68
        )
69
        json.dump(export_site(**form.cleaned_data), response, indent=2)
70
        return response
71

  
72

  
73
config_export = ConfigExportView.as_view()
74

  
75

  
76
class ConfigImportView(FormView):
77
    form_class = ImportForm
78
    template_name = 'lingo/pricing/import.html'
79
    success_url = reverse_lazy('lingo-manager-pricing-list')
80

  
81
    def dispatch(self, request, *args, **kwargs):
82
        if not request.user.is_staff:
83
            raise PermissionDenied()
84
        return super().dispatch(request, *args, **kwargs)
85

  
86
    def form_valid(self, form):
87
        try:
88
            config_json = json.loads(force_text(self.request.FILES['config_json'].read()))
89
        except ValueError:
90
            form.add_error('config_json', _('File is not in the expected JSON format.'))
91
            return self.form_invalid(form)
92

  
93
        try:
94
            results = import_site(config_json, overwrite=False)
95
        except AgendaImportError as exc:
96
            form.add_error('config_json', '%s' % exc)
97
            return self.form_invalid(form)
98
        except KeyError as exc:
99
            form.add_error('config_json', _('Key "%s" is missing.') % exc.args[0])
100
            return self.form_invalid(form)
101

  
102
        import_messages = {
103
            'agendas': {
104
                'update_noop': _('No agenda updated.'),
105
                'update': lambda x: ungettext(
106
                    'An agenda has been updated.',
107
                    '%(count)d agendas have been updated.',
108
                    x,
109
                ),
110
            },
111
            'pricing_categories': {
112
                'create_noop': _('No pricing criteria category created.'),
113
                'create': lambda x: ungettext(
114
                    'A pricing criteria category has been created.',
115
                    '%(count)d pricing criteria categories have been created.',
116
                    x,
117
                ),
118
                'update_noop': _('No pricing criteria category updated.'),
119
                'update': lambda x: ungettext(
120
                    'A pricing criteria category has been updated.',
121
                    '%(count)d pricing criteria categories have been updated.',
122
                    x,
123
                ),
124
            },
125
            'pricing_models': {
126
                'create_noop': _('No pricing model created.'),
127
                'create': lambda x: ungettext(
128
                    'A pricing model has been created.',
129
                    '%(count)d pricing models have been created.',
130
                    x,
131
                ),
132
                'update_noop': _('No pricing model updated.'),
133
                'update': lambda x: ungettext(
134
                    'A pricing model has been updated.',
135
                    '%(count)d pricing models have been updated.',
136
                    x,
137
                ),
138
            },
139
        }
140

  
141
        global_noop = True
142
        for obj_name, obj_results in results.items():
143
            if obj_results['all']:
144
                global_noop = False
145
                count = len(obj_results['created'])
146
                if not count:
147
                    message1 = import_messages[obj_name]['create_noop']
148
                else:
149
                    message1 = import_messages[obj_name]['create'](count) % {'count': count}
150

  
151
                count = len(obj_results['updated'])
152
                if not count:
153
                    message2 = import_messages[obj_name]['update_noop']
154
                else:
155
                    message2 = import_messages[obj_name]['update'](count) % {'count': count}
156

  
157
                obj_results['messages'] = "%s %s" % (message1, message2)
158

  
159
        pc_count, pm_count = (
160
            len(results['pricing_categories']['all']),
161
            len(results['pricing_models']['all']),
162
        )
163
        if (pc_count, pm_count) == (1, 0):
164
            # only one criteria category imported, redirect to criteria page
165
            return HttpResponseRedirect(reverse('lingo-manager-pricing-criteria-list'))
166
        if (pc_count, pm_count) == (0, 1):
167
            # only one pricing imported, redirect to pricing page
168
            return HttpResponseRedirect(
169
                reverse(
170
                    'lingo-manager-pricing-detail',
171
                    kwargs={'pk': results['pricing_models']['all'][0].pk},
172
                )
173
            )
174

  
175
        if global_noop:
176
            messages.info(self.request, _('No data found.'))
177
        else:
178
            messages.info(self.request, results['agendas']['messages'])
179
            messages.info(self.request, results['pricing_categories']['messages'])
180
            messages.info(self.request, results['pricing_models']['messages'])
181

  
182
        return super().form_valid(form)
183

  
184

  
185
config_import = ConfigImportView.as_view()
44 186

  
45 187

  
46 188
class PricingListView(ListView):
tests/pricing/test_import_export.py
1
import copy
2
import datetime
3
import json
4
import os
5
import shutil
6
import sys
7
import tempfile
8
from io import StringIO
9

  
10
import pytest
11
from django.core.management import call_command
12
from django.utils.encoding import force_bytes
13

  
14
from lingo.agendas.models import Agenda
15
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
16
from lingo.pricing.utils import import_site
17
from lingo.utils.misc import AgendaImportError
18

  
19
pytestmark = pytest.mark.django_db
20

  
21

  
22
def get_output_of_command(command, *args, **kwargs):
23
    old_stdout = sys.stdout
24
    output = sys.stdout = StringIO()
25
    call_command(command, *args, **kwargs)
26
    sys.stdout = old_stdout
27
    return output.getvalue()
28

  
29

  
30
def test_import_export(app):
31
    agenda = Agenda.objects.create(label='Foo Bar')
32
    pricing = Pricing.objects.create(label='Foo')
33
    AgendaPricing.objects.create(
34
        agenda=agenda,
35
        pricing=pricing,
36
        date_start=datetime.date(year=2021, month=9, day=1),
37
        date_end=datetime.date(year=2021, month=10, day=1),
38
    )
39
    CriteriaCategory.objects.create(label='Foo bar')
40

  
41
    output = get_output_of_command('export_site')
42
    assert len(json.loads(output)['agendas']) == 1
43
    assert len(json.loads(output)['pricing_models']) == 1
44
    assert len(json.loads(output)['pricing_categories']) == 1
45
    import_site(data={}, clean=True)
46
    empty_output = get_output_of_command('export_site')
47
    assert len(json.loads(empty_output)['agendas']) == 1
48
    assert len(json.loads(empty_output)['agendas'][0]['pricings']) == 0
49
    assert len(json.loads(empty_output)['pricing_models']) == 0
50
    assert len(json.loads(empty_output)['pricing_categories']) == 0
51

  
52
    old_stdin = sys.stdin
53
    sys.stdin = StringIO(json.dumps({}))
54
    agenda = Agenda.objects.create(label='Foo Bar')
55
    pricing = Pricing.objects.create(label='Foo')
56
    AgendaPricing.objects.create(
57
        agenda=agenda,
58
        pricing=pricing,
59
        date_start=datetime.date(year=2021, month=9, day=1),
60
        date_end=datetime.date(year=2021, month=10, day=1),
61
    )
62
    CriteriaCategory.objects.create(label='Foo bar')
63
    old_stdin = sys.stdin
64
    sys.stdin = StringIO(json.dumps({}))
65
    assert AgendaPricing.objects.count() == 1
66
    assert Pricing.objects.count() == 1
67
    assert CriteriaCategory.objects.count() == 1
68
    try:
69
        call_command('import_site', '-', clean=True)
70
    finally:
71
        sys.stdin = old_stdin
72
    assert AgendaPricing.objects.count() == 0
73
    assert Pricing.objects.count() == 0
74
    assert CriteriaCategory.objects.count() == 0
75

  
76
    with tempfile.NamedTemporaryFile() as f:
77
        f.write(force_bytes(output))
78
        f.flush()
79
        call_command('import_site', f.name)
80
    assert AgendaPricing.objects.count() == 1
81
    assert Pricing.objects.count() == 1
82
    assert CriteriaCategory.objects.count() == 1
83

  
84
    import_site(data={}, if_empty=True)
85
    assert AgendaPricing.objects.count() == 1
86
    assert Pricing.objects.count() == 1
87
    assert CriteriaCategory.objects.count() == 1
88

  
89
    import_site(data={}, clean=True)
90
    tempdir = tempfile.mkdtemp('lingo-test')
91
    empty_output = get_output_of_command('export_site', output=os.path.join(tempdir, 't.json'))
92
    assert os.path.exists(os.path.join(tempdir, 't.json'))
93
    shutil.rmtree(tempdir)
94

  
95

  
96
def test_import_export_agenda_with_pricing(app):
97
    pricing = Pricing.objects.create(label='Foo')
98
    agenda = Agenda.objects.create(label='Foo Bar')
99
    agenda_pricing = AgendaPricing.objects.create(
100
        agenda=agenda,
101
        pricing=pricing,
102
        date_start=datetime.date(year=2021, month=9, day=1),
103
        date_end=datetime.date(year=2021, month=10, day=1),
104
        pricing_data={
105
            'foo': 'bar',
106
        },
107
    )
108
    output = get_output_of_command('export_site')
109

  
110
    import_site(data={}, clean=True)
111
    assert Pricing.objects.count() == 0
112
    data = json.loads(output)
113
    del data['pricing_models']
114

  
115
    with pytest.raises(AgendaImportError) as excinfo:
116
        import_site(data, overwrite=True)
117
    assert str(excinfo.value) == 'Missing "foo" pricing model'
118

  
119
    Pricing.objects.create(label='foobar')
120
    with pytest.raises(AgendaImportError) as excinfo:
121
        import_site(data, overwrite=True)
122
    assert str(excinfo.value) == 'Missing "foo" pricing model'
123

  
124
    Agenda.objects.all().delete()
125
    pricing = Pricing.objects.create(label='Foo')
126
    with pytest.raises(AgendaImportError) as excinfo:
127
        import_site(data, overwrite=True)
128
    assert str(excinfo.value) == 'Missing "foo-bar" agenda'
129

  
130
    agenda = Agenda.objects.create(label='Foo Bar')
131
    import_site(data, overwrite=True)
132
    assert agenda.agendapricing_set.count() == 1
133
    agenda_pricing = agenda.agendapricing_set.get()
134
    assert agenda_pricing.agenda == agenda
135
    assert agenda_pricing.pricing == pricing
136
    assert agenda_pricing.date_start == datetime.date(year=2021, month=9, day=1)
137
    assert agenda_pricing.date_end == datetime.date(year=2021, month=10, day=1)
138

  
139
    # again
140
    import_site(data)
141
    assert agenda.agendapricing_set.count() == 1
142
    agenda_pricing = AgendaPricing.objects.get(pk=agenda_pricing.pk)
143
    assert agenda_pricing.agenda == agenda
144
    assert agenda_pricing.pricing == pricing
145

  
146
    data['agendas'][0]['pricings'].append(
147
        {
148
            'pricing': 'foo',
149
            'date_start': '2022-09-01',
150
            'date_end': '2022-10-01',
151
            'pricing_data': {'foo': 'bar'},
152
        }
153
    )
154
    import_site(data)
155
    assert agenda.agendapricing_set.count() == 2
156
    agenda_pricing = AgendaPricing.objects.latest('pk')
157
    assert agenda_pricing.agenda == agenda
158
    assert agenda_pricing.pricing == pricing
159
    assert agenda_pricing.date_start == datetime.date(year=2022, month=9, day=1)
160
    assert agenda_pricing.date_end == datetime.date(year=2022, month=10, day=1)
161

  
162

  
163
def test_import_export_site_criteria_category(app):
164
    output = get_output_of_command('export_site')
165
    payload = json.loads(output)
166
    assert len(payload['pricing_categories']) == 0
167

  
168
    category = CriteriaCategory.objects.create(label='Foo bar')
169
    Criteria.objects.create(label='Foo reason', category=category)
170
    Criteria.objects.create(label='Baz', category=category)
171

  
172
    output = get_output_of_command('export_site')
173
    payload = json.loads(output)
174
    assert len(payload['pricing_categories']) == 1
175

  
176
    category.delete()
177
    assert not CriteriaCategory.objects.exists()
178
    assert not Criteria.objects.exists()
179

  
180
    import_site(copy.deepcopy(payload))
181
    assert CriteriaCategory.objects.count() == 1
182
    category = CriteriaCategory.objects.first()
183
    assert category.label == 'Foo bar'
184
    assert category.slug == 'foo-bar'
185
    assert category.criterias.count() == 2
186
    assert Criteria.objects.get(category=category, label='Foo reason', slug='foo-reason')
187
    assert Criteria.objects.get(category=category, label='Baz', slug='baz')
188

  
189
    # update
190
    update_payload = copy.deepcopy(payload)
191
    update_payload['pricing_categories'][0]['label'] = 'Foo bar Updated'
192
    import_site(update_payload)
193
    category.refresh_from_db()
194
    assert category.label == 'Foo bar Updated'
195

  
196
    # insert another category
197
    category.slug = 'foo-bar-updated'
198
    category.save()
199
    import_site(copy.deepcopy(payload))
200
    assert CriteriaCategory.objects.count() == 2
201
    category = CriteriaCategory.objects.latest('pk')
202
    assert category.label == 'Foo bar'
203
    assert category.slug == 'foo-bar'
204
    assert category.criterias.count() == 2
205
    assert Criteria.objects.get(category=category, label='Foo reason', slug='foo-reason')
206
    assert Criteria.objects.get(category=category, label='Baz', slug='baz')
207

  
208
    # with overwrite
209
    Criteria.objects.create(category=category, label='Baz2')
210
    import_site(copy.deepcopy(payload), overwrite=True)
211
    assert CriteriaCategory.objects.count() == 2
212
    category = CriteriaCategory.objects.latest('pk')
213
    assert category.label == 'Foo bar'
214
    assert category.slug == 'foo-bar'
215
    assert category.criterias.count() == 2
216
    assert Criteria.objects.get(category=category, label='Foo reason', slug='foo-reason')
217
    assert Criteria.objects.get(category=category, label='Baz', slug='baz')
218

  
219

  
220
def test_import_export_site(app):
221
    output = get_output_of_command('export_site')
222
    payload = json.loads(output)
223
    assert len(payload['pricing_models']) == 0
224

  
225
    pricing = Pricing.objects.create(label='Foo bar', extra_variables={'foo': 'bar'})
226

  
227
    output = get_output_of_command('export_site')
228
    payload = json.loads(output)
229
    assert len(payload['pricing_models']) == 1
230

  
231
    pricing.delete()
232
    assert not Pricing.objects.exists()
233

  
234
    import_site(copy.deepcopy(payload))
235
    assert Pricing.objects.count() == 1
236
    pricing = Pricing.objects.first()
237
    assert pricing.label == 'Foo bar'
238
    assert pricing.slug == 'foo-bar'
239
    assert pricing.extra_variables == {'foo': 'bar'}
240

  
241
    # update
242
    update_payload = copy.deepcopy(payload)
243
    update_payload['pricing_models'][0]['label'] = 'Foo bar Updated'
244
    import_site(update_payload)
245
    pricing.refresh_from_db()
246
    assert pricing.label == 'Foo bar Updated'
247

  
248
    # insert another pricing
249
    pricing.slug = 'foo-bar-updated'
250
    pricing.save()
251
    import_site(copy.deepcopy(payload))
252
    assert Pricing.objects.count() == 2
253
    pricing = Pricing.objects.latest('pk')
254
    assert pricing.label == 'Foo bar'
255
    assert pricing.slug == 'foo-bar'
256
    assert pricing.extra_variables == {'foo': 'bar'}
257

  
258

  
259
def test_import_export_site_with_categories(app):
260
    pricing = Pricing.objects.create(label='Foo bar')
261
    category = CriteriaCategory.objects.create(label='Foo bar')
262
    pricing.categories.add(category, through_defaults={'order': 42})
263

  
264
    output = get_output_of_command('export_site')
265

  
266
    import_site(data={}, clean=True)
267
    assert Pricing.objects.count() == 0
268
    assert CriteriaCategory.objects.count() == 0
269
    data = json.loads(output)
270
    del data['pricing_categories']
271

  
272
    with pytest.raises(AgendaImportError) as excinfo:
273
        import_site(data, overwrite=True)
274
    assert str(excinfo.value) == 'Missing "foo-bar" pricing category'
275

  
276
    CriteriaCategory.objects.create(label='Foobar')
277
    with pytest.raises(AgendaImportError) as excinfo:
278
        import_site(data, overwrite=True)
279
    assert str(excinfo.value) == 'Missing "foo-bar" pricing category'
280

  
281
    category = CriteriaCategory.objects.create(label='Foo bar')
282
    import_site(data, overwrite=True)
283
    pricing = Pricing.objects.get(slug=pricing.slug)
284
    assert list(pricing.categories.all()) == [category]
285
    assert PricingCriteriaCategory.objects.first().order == 42
286

  
287
    category2 = CriteriaCategory.objects.create(label='Foo bar 2')
288
    category3 = CriteriaCategory.objects.create(label='Foo bar 3')
289
    pricing.categories.add(category2, through_defaults={'order': 1})
290
    output = get_output_of_command('export_site')
291
    data = json.loads(output)
292
    del data['pricing_categories']
293
    data['pricing_models'][0]['categories'] = [
294
        {
295
            'category': 'foo-bar-3',
296
            'order': 1,
297
            'criterias': [],
298
        },
299
        {
300
            'category': 'foo-bar',
301
            'order': 35,
302
            'criterias': [],
303
        },
304
    ]
305
    import_site(data, overwrite=True)
306
    assert list(pricing.categories.all()) == [category, category3]
307
    assert list(
308
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
309
    ) == [category3.pk, category.pk]
310
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
311
        1,
312
        35,
313
    ]
314
    assert list(pricing.criterias.all()) == []
315

  
316
    criteria1 = Criteria.objects.create(label='Crit 1', category=category)
317
    Criteria.objects.create(label='Crit 2', category=category)
318
    criteria3 = Criteria.objects.create(label='Crit 3', category=category)
319

  
320
    # unknown criteria
321
    data['pricing_models'][0]['categories'] = [
322
        {
323
            'category': 'foo-bar-3',
324
            'order': 1,
325
            'criterias': ['unknown'],
326
        },
327
        {
328
            'category': 'foo-bar',
329
            'order': 35,
330
            'criterias': [],
331
        },
332
    ]
333
    with pytest.raises(AgendaImportError) as excinfo:
334
        import_site(data, overwrite=True)
335
    assert str(excinfo.value) == 'Missing "unknown" pricing criteria for "foo-bar-3" category'
336

  
337
    # wrong criteria (from another category)
338
    data['pricing_models'][0]['categories'] = [
339
        {
340
            'category': 'foo-bar-3',
341
            'order': 1,
342
            'criterias': ['crit-1'],
343
        },
344
        {
345
            'category': 'foo-bar',
346
            'order': 35,
347
            'criterias': [],
348
        },
349
    ]
350
    with pytest.raises(AgendaImportError) as excinfo:
351
        import_site(data, overwrite=True)
352
    assert str(excinfo.value) == 'Missing "crit-1" pricing criteria for "foo-bar-3" category'
353

  
354
    data['pricing_models'][0]['categories'] = [
355
        {
356
            'category': 'foo-bar-3',
357
            'order': 1,
358
            'criterias': [],
359
        },
360
        {
361
            'category': 'foo-bar',
362
            'order': 35,
363
            'criterias': ['crit-1', 'crit-3'],
364
        },
365
    ]
366
    import_site(data, overwrite=True)
367
    assert list(pricing.categories.all()) == [category, category3]
368
    assert list(
369
        PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('category', flat=True)
370
    ) == [category3.pk, category.pk]
371
    assert list(PricingCriteriaCategory.objects.filter(pricing=pricing).values_list('order', flat=True)) == [
372
        1,
373
        35,
374
    ]
375
    assert set(pricing.criterias.all()) == {criteria1, criteria3}
tests/pricing/test_manager.py
20 20
    return agenda
21 21

  
22 22

  
23
def test_export_site(settings, freezer, app, admin_user):
24
    freezer.move_to('2020-06-15')
25
    login(app)
26
    resp = app.get('/manage/pricing/')
27
    resp = resp.click('Export')
28

  
29
    resp = resp.form.submit()
30
    assert resp.headers['content-type'] == 'application/json'
31
    assert resp.headers['content-disposition'] == 'attachment; filename="export_pricing_config_20200615.json"'
32

  
33
    site_json = json.loads(resp.text)
34
    assert site_json == {
35
        'agendas': [],
36
        'pricing_categories': [],
37
        'pricing_models': [],
38
    }
39

  
40
    Agenda.objects.create(label='Foo Bar')
41
    resp = app.get('/manage/pricing/export/')
42
    resp = resp.form.submit()
43

  
44
    site_json = json.loads(resp.text)
45
    assert len(site_json['agendas']) == 1
46

  
47
    resp = app.get('/manage/pricing/export/')
48
    resp.form['agendas'] = False
49
    resp.form['pricing_categories'] = False
50
    resp.form['pricing_models'] = False
51
    resp = resp.form.submit()
52

  
53
    site_json = json.loads(resp.text)
54
    assert 'agendas' not in site_json
55
    assert 'pricing_categories' not in site_json
56
    assert 'pricing_models' not in site_json
57

  
58

  
23 59
@pytest.mark.xfail(reason='/manage/ limited to admin')
24 60
def test_list_pricings_as_manager(app, manager_user, agenda_with_restrictions):
25 61
    app = login(app, username='manager', password='manager')
......
144 180
    app.get('/manage/pricing/%s/duplicate/' % pricing.pk, status=403)
145 181

  
146 182

  
147
@pytest.mark.xfail(reason='import not yet implemented')
148 183
@pytest.mark.freeze_time('2021-07-08')
149 184
def test_import_pricing(app, admin_user):
150 185
    pricing = Pricing.objects.create(label='Model')
......
156 191
    pricing_export = resp.text
157 192

  
158 193
    # existing pricing
159
    resp = app.get('/manage/', status=200)
194
    resp = app.get('/manage/pricing/', status=200)
160 195
    resp = resp.click('Import')
161
    resp.form['agendas_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
196
    resp.form['config_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
162 197
    resp = resp.form.submit()
163 198
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
164 199
    resp = resp.follow()
......
167 202

  
168 203
    # new pricing
169 204
    Pricing.objects.all().delete()
170
    resp = app.get('/manage/', status=200)
205
    resp = app.get('/manage/pricing/', status=200)
171 206
    resp = resp.click('Import')
172
    resp.form['agendas_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
207
    resp.form['config_json'] = Upload('export.json', pricing_export.encode('utf-8'), 'application/json')
173 208
    resp = resp.form.submit()
174 209
    pricing = Pricing.objects.latest('pk')
175 210
    assert resp.location.endswith('/manage/pricing/%s/' % pricing.pk)
......
186 221
    pricings['pricing_models'][2]['label'] = 'Foo bar 3'
187 222
    pricings['pricing_models'][2]['slug'] = 'foo-bar-3'
188 223

  
189
    resp = app.get('/manage/', status=200)
224
    resp = app.get('/manage/pricing/', status=200)
190 225
    resp = resp.click('Import')
191
    resp.form['agendas_json'] = Upload(
192
        'export.json', json.dumps(pricings).encode('utf-8'), 'application/json'
193
    )
226
    resp.form['config_json'] = Upload('export.json', json.dumps(pricings).encode('utf-8'), 'application/json')
194 227
    resp = resp.form.submit()
195
    assert resp.location.endswith('/manage/')
228
    assert resp.location.endswith('/manage/pricing/')
196 229
    resp = resp.follow()
197 230
    assert '2 pricing models have been created. A pricing model has been updated.' in resp.text
198 231
    assert Pricing.objects.count() == 3
199 232

  
200 233
    Pricing.objects.all().delete()
201
    resp = app.get('/manage/', status=200)
234
    resp = app.get('/manage/pricing/', status=200)
202 235
    resp = resp.click('Import')
203
    resp.form['agendas_json'] = Upload(
204
        'export.json', json.dumps(pricings).encode('utf-8'), 'application/json'
205
    )
236
    resp.form['config_json'] = Upload('export.json', json.dumps(pricings).encode('utf-8'), 'application/json')
206 237
    resp = resp.form.submit().follow()
207 238
    assert '3 pricing models have been created. No pricing model updated.' in resp.text
208 239
    assert Pricing.objects.count() == 3
......
734 765
    app.get('/manage/pricing/criteria/category/%s/order/' % (category.pk), status=403)
735 766

  
736 767

  
737
@pytest.mark.xfail(reason='import not yet implemented')
738 768
@pytest.mark.freeze_time('2021-07-08')
739 769
def test_import_criteria_category(app, admin_user):
740 770
    category = CriteriaCategory.objects.create(label='Foo bar')
......
751 781
    category_export = resp.text
752 782

  
753 783
    # existing category
754
    resp = app.get('/manage/', status=200)
784
    resp = app.get('/manage/pricing/', status=200)
755 785
    resp = resp.click('Import')
756
    resp.form['agendas_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
786
    resp.form['config_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
757 787
    resp = resp.form.submit()
758 788
    assert resp.location.endswith('/manage/pricing/criterias/')
759 789
    resp = resp.follow()
......
765 795

  
766 796
    # new category
767 797
    CriteriaCategory.objects.all().delete()
768
    resp = app.get('/manage/', status=200)
798
    resp = app.get('/manage/pricing/', status=200)
769 799
    resp = resp.click('Import')
770
    resp.form['agendas_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
800
    resp.form['config_json'] = Upload('export.json', category_export.encode('utf-8'), 'application/json')
771 801
    resp = resp.form.submit()
772 802
    assert resp.location.endswith('/manage/pricing/criterias/')
773 803
    resp = resp.follow()
......
786 816
    categories['pricing_categories'][2]['label'] = 'Foo bar 3'
787 817
    categories['pricing_categories'][2]['slug'] = 'foo-bar-3'
788 818

  
789
    resp = app.get('/manage/', status=200)
819
    resp = app.get('/manage/pricing/', status=200)
790 820
    resp = resp.click('Import')
791
    resp.form['agendas_json'] = Upload(
821
    resp.form['config_json'] = Upload(
792 822
        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
793 823
    )
794 824
    resp = resp.form.submit()
795
    assert resp.location.endswith('/manage/')
825
    assert resp.location.endswith('/manage/pricing/')
796 826
    resp = resp.follow()
797 827
    assert (
798 828
        '2 pricing criteria categories have been created. A pricing criteria category has been updated.'
......
802 832
    assert Criteria.objects.count() == 6
803 833

  
804 834
    CriteriaCategory.objects.all().delete()
805
    resp = app.get('/manage/', status=200)
835
    resp = app.get('/manage/pricing/', status=200)
806 836
    resp = resp.click('Import')
807
    resp.form['agendas_json'] = Upload(
837
    resp.form['config_json'] = Upload(
808 838
        'export.json', json.dumps(categories).encode('utf-8'), 'application/json'
809 839
    )
810 840
    resp = resp.form.submit().follow()
811
-