Projet

Général

Profil

0001-misc-remove-jsonfield-requirement-53289.patch

Lauréline Guérin, 20 avril 2021 16:24

Télécharger (13,8 ko)

Voir les différences:

Subject: [PATCH] misc: remove jsonfield requirement (#53289)

 .../management/commands/ensure_jsonb.py       | 49 +++++++++++++++++
 chrono/agendas/migrations/0003_booking.py     |  4 +-
 .../migrations/0056_auto_20200811_1611.py     |  4 +-
 .../migrations/0075_auto_20210216_1553.py     |  4 +-
 .../agendas/migrations/0079_text_to_jsonb.py  | 16 ++++++
 chrono/agendas/models.py                      |  7 ++-
 chrono/utils/db.py                            | 49 +++++++++++++++++
 requirements.txt                              |  1 -
 setup.py                                      |  1 -
 tests/test_ensure_jsonbfields.py              | 52 +++++++++++++++++++
 10 files changed, 175 insertions(+), 12 deletions(-)
 create mode 100644 chrono/agendas/management/commands/ensure_jsonb.py
 create mode 100644 chrono/agendas/migrations/0079_text_to_jsonb.py
 create mode 100644 chrono/utils/db.py
 create mode 100644 tests/test_ensure_jsonbfields.py
chrono/agendas/management/commands/ensure_jsonb.py
1
# chrono - agendas system
2
# Copyright (C) 2021  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
from django.apps import apps
18
from django.contrib.postgres.fields import JSONField
19
from django.core.management.base import BaseCommand, CommandError
20
from django.db import connection
21

  
22

  
23
class Command(BaseCommand):
24
    help = 'Ensure all JSON fields are of type jsonb'
25

  
26
    def handle(self, **options):
27
        for app in apps.get_models():
28
            for field in app._meta.get_fields():
29
                if isinstance(field, JSONField):
30
                    table_name = app._meta.db_table
31
                    column_name = app._meta.get_field(field.name).column
32
                    with connection.cursor() as cursor:
33
                        query = '''SELECT table_schema
34
                                     FROM information_schema.columns
35
                                    WHERE table_name = %s AND column_name = %s AND data_type != %s'''
36
                        cursor.execute(query, [table_name, column_name, 'jsonb'])
37
                        for line in cursor.fetchall():
38
                            alter_query = '''ALTER TABLE "%(schema_name)s"."%(table_name)s"
39
                                            ALTER COLUMN "%(column_name)s"
40
                                                    TYPE jsonb USING "%(column_name)s"::jsonb'''
41
                            params = {
42
                                'schema_name': line[0],
43
                                'table_name': table_name,
44
                                'column_name': column_name,
45
                            }
46
                            try:
47
                                cursor.execute(alter_query % params)
48
                            except Exception as e:
49
                                raise CommandError(e)
chrono/agendas/migrations/0003_booking.py
1 1
# -*- coding: utf-8 -*-
2 2
from __future__ import unicode_literals
3 3

  
4
import jsonfield.fields
4
from django.contrib.postgres.fields import JSONField
5 5
from django.db import migrations, models
6 6

  
7 7

  
......
19 19
                    'id',
20 20
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
21 21
                ),
22
                ('extra_data', jsonfield.fields.JSONField(null=True)),
22
                ('extra_data', JSONField(null=True)),
23 23
                ('event', models.ForeignKey(to='agendas.Event', on_delete=models.CASCADE)),
24 24
            ],
25 25
            options={},
chrono/agendas/migrations/0056_auto_20200811_1611.py
3 3
from __future__ import unicode_literals
4 4

  
5 5
import django.db.models.deletion
6
import jsonfield.fields
6
from django.contrib.postgres.fields import JSONField
7 7
from django.db import migrations, models
8 8

  
9 9

  
......
23 23
                ),
24 24
                ('timestamp', models.DateTimeField(auto_now_add=True)),
25 25
                ('seen', models.BooleanField(default=False)),
26
                ('booking_errors', jsonfield.fields.JSONField(default=dict)),
26
                ('booking_errors', JSONField(default=dict)),
27 27
                ('bookings', models.ManyToManyField(to='agendas.Booking')),
28 28
            ],
29 29
            options={
chrono/agendas/migrations/0075_auto_20210216_1553.py
3 3
from __future__ import unicode_literals
4 4

  
5 5
import django.db.models.deletion
6
import jsonfield.fields
6
from django.contrib.postgres.fields import JSONField
7 7
from django.db import migrations, models
8 8

  
9 9

  
......
27 27
        migrations.AddField(
28 28
            model_name='event',
29 29
            name='recurrence_rule',
30
            field=jsonfield.fields.JSONField(null=True, verbose_name='Recurrence rule'),
30
            field=JSONField(null=True, verbose_name='Recurrence rule', blank=True),
31 31
        ),
32 32
        migrations.AddField(
33 33
            model_name='event',
chrono/agendas/migrations/0079_text_to_jsonb.py
1
from django.db import migrations
2

  
3
from chrono.utils.db import EnsureJsonbType
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('agendas', '0078_absence_reasons'),
10
    ]
11

  
12
    operations = [
13
        EnsureJsonbType(model_name='booking', field_name='extra_data'),
14
        EnsureJsonbType(model_name='event', field_name='recurrence_rule'),
15
        EnsureJsonbType(model_name='eventcancellationreport', field_name='booking_errors'),
16
    ]
chrono/agendas/models.py
30 30
from dateutil.rrule import DAILY, WEEKLY, rrule, rruleset
31 31
from django.conf import settings
32 32
from django.contrib.auth.models import Group
33
from django.contrib.postgres.fields import ArrayField
33
from django.contrib.postgres.fields import ArrayField, JSONField
34 34
from django.core.exceptions import FieldDoesNotExist, ValidationError
35 35
from django.core.validators import MaxValueValidator, MinValueValidator
36 36
from django.db import connection, models, transaction
......
47 47
from django.utils.translation import ugettext
48 48
from django.utils.translation import ugettext_lazy as _
49 49
from django.utils.translation import ungettext
50
from jsonfield import JSONField
51 50

  
52 51
from chrono.interval import Interval, IntervalSet
53 52
from chrono.utils.requests_wrapper import requests as requests_wrapper
......
1089 1088
    agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
1090 1089
    start_datetime = models.DateTimeField(_('Date/time'))
1091 1090
    repeat = models.CharField(_('Repeat'), max_length=16, blank=True, choices=REPEAT_CHOICES)
1092
    recurrence_rule = JSONField(_('Recurrence rule'), null=True)
1091
    recurrence_rule = JSONField(_('Recurrence rule'), null=True, blank=True)
1093 1092
    recurrence_end_date = models.DateField(_('Recurrence end date'), null=True, blank=True)
1094 1093
    primary_event = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='recurrences')
1095 1094
    duration = models.PositiveIntegerField(_('Duration (in minutes)'), default=None, null=True, blank=True)
......
2251 2250
    timestamp = models.DateTimeField(auto_now_add=True)
2252 2251
    seen = models.BooleanField(default=False)
2253 2252
    bookings = models.ManyToManyField(Booking)
2254
    booking_errors = JSONField()
2253
    booking_errors = JSONField(default=dict)
2255 2254

  
2256 2255
    def __str__(self):
2257 2256
        return '%s - %s' % (self.timestamp.strftime('%Y-%m-%d %H:%M:%S'), self.event)
chrono/utils/db.py
1
# chrono - agendas system
2
# Copyright (C) 2021  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
from django.db import connection
18
from django.db.migrations.operations.base import Operation
19

  
20

  
21
class EnsureJsonbType(Operation):
22

  
23
    reversible = True
24

  
25
    def __init__(self, model_name, field_name):
26
        self.model_name = model_name
27
        self.field_name = field_name
28

  
29
    def state_forwards(self, app_label, state):
30
        pass
31

  
32
    def database_forwards(self, app_label, schema_editor, from_state, to_state):
33
        if connection.vendor == 'postgresql':
34
            model = from_state.apps.get_model(app_label, self.model_name)
35
            table_name = model._meta.db_table
36
            field = model._meta.get_field(self.field_name)
37
            _, column_name = field.get_attname_column()
38
            with schema_editor.connection.cursor() as cursor:
39
                cursor.execute(
40
                    'ALTER TABLE {table} ALTER COLUMN {col} TYPE jsonb USING {col}::jsonb;'.format(
41
                        table=table_name, col=column_name
42
                    )
43
                )
44

  
45
    def database_backwards(self, app_label, schema_editor, from_state, to_state):
46
        pass
47

  
48
    def describe(self):
49
        return "Migrate to postgres jsonb type"
requirements.txt
1 1
django>=1.8, <1.9
2 2
gadjo
3 3
djangorestframework>=3.1, <3.7
4
django-jsonfield >= 0.9.3
5 4
requests
6 5
vobject
setup.py
165 165
        'gadjo',
166 166
        'djangorestframework>=3.4',
167 167
        'django-filter',
168
        'django-jsonfield >= 0.9.3',
169 168
        'vobject',
170 169
        'python-dateutil',
171 170
        'requests',
tests/test_ensure_jsonbfields.py
1
# -*- coding: utf-8 -*-
2

  
3
import pytest
4
from django.core.management import call_command
5
from django.db import connection
6

  
7
pytestmark = pytest.mark.django_db
8

  
9

  
10
@pytest.mark.skipif(connection.vendor != 'postgresql', reason='only postgresql is supported')
11
def test_ensure_jsonb_fields():
12
    json_fields = (
13
        'extra_data',
14
        'booking_errors',
15
        'recurrence_rule',
16
    )
17

  
18
    with connection.cursor() as cursor:
19
        query = '''SELECT table_name, column_name, data_type
20
                     FROM information_schema.columns
21
                    WHERE column_name IN %(json_fields)s'''
22
        cursor.execute(query, {'json_fields': json_fields})
23

  
24
        # make sure the data_type is correct
25
        for line in cursor.fetchall():
26
            assert line[2] == 'jsonb'
27

  
28
        # alter columns
29
        cursor.execute(
30
            '''ALTER TABLE agendas_booking
31
              ALTER COLUMN extra_data TYPE text USING extra_data::text'''
32
        )
33
        cursor.execute(
34
            '''ALTER TABLE agendas_eventcancellationreport
35
              ALTER COLUMN booking_errors TYPE text USING booking_errors::text'''
36
        )
37
        cursor.execute(
38
            '''ALTER TABLE agendas_event
39
              ALTER COLUMN recurrence_rule TYPE text USING recurrence_rule::text'''
40
        )
41

  
42
    call_command('ensure_jsonb')
43

  
44
    with connection.cursor() as cursor:
45
        query = '''SELECT table_name, column_name, data_type
46
                     FROM information_schema.columns
47
                    WHERE column_name IN %(json_fields)s'''
48
        cursor.execute(query, {'json_fields': json_fields})
49

  
50
        # check the data_type is correct
51
        for line in cursor.fetchall():
52
            assert line[2] == 'jsonb'
0
-