diff options
16 files changed, 173 insertions, 222 deletions
diff --git a/archaeological_context_records/migrations/0123_add_timezone_django_v4.py b/archaeological_context_records/migrations/0123_add_timezone_django_v4.py index 5e9bf1249..55f164d65 100644 --- a/archaeological_context_records/migrations/0123_add_timezone_django_v4.py +++ b/archaeological_context_records/migrations/0123_add_timezone_django_v4.py @@ -10,7 +10,7 @@ import re class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0267_add_timezone_django_v4'), + ('ishtar_common', '0268_add_timezone_django_v4'), ('archaeological_context_records', '0122_cr_ishtar_users'), ] diff --git a/archaeological_files/migrations/0120_add_timezone_django_v4.py b/archaeological_files/migrations/0120_add_timezone_django_v4.py index 910efb28b..5a5eb7400 100644 --- a/archaeological_files/migrations/0120_add_timezone_django_v4.py +++ b/archaeological_files/migrations/0120_add_timezone_django_v4.py @@ -9,7 +9,7 @@ import re class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0267_add_timezone_django_v4'), + ('ishtar_common', '0268_add_timezone_django_v4'), ('archaeological_files', '0119_file_ishtar_users'), ] diff --git a/archaeological_finds/migrations/0142_add_timezone_django_v4.py b/archaeological_finds/migrations/0142_add_timezone_django_v4.py index 2a59695f1..d2cf2da1a 100644 --- a/archaeological_finds/migrations/0142_add_timezone_django_v4.py +++ b/archaeological_finds/migrations/0142_add_timezone_django_v4.py @@ -11,7 +11,7 @@ import re class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0267_add_timezone_django_v4'), + ('ishtar_common', '0268_add_timezone_django_v4'), ('archaeological_finds', '0141_data_migration_recommanded_treatments'), ] diff --git a/archaeological_operations/migrations/0123_add_timezone_django_v4.py b/archaeological_operations/migrations/0123_add_timezone_django_v4.py index 3c60ba456..7bc472523 100644 --- a/archaeological_operations/migrations/0123_add_timezone_django_v4.py +++ b/archaeological_operations/migrations/0123_add_timezone_django_v4.py @@ -10,7 +10,7 @@ import re class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0267_add_timezone_django_v4'), + ('ishtar_common', '0268_add_timezone_django_v4'), ('archaeological_operations', '0122_admin_act_many_intented_to'), ] diff --git a/archaeological_warehouse/migrations/0126_add_timezone_django_v4.py b/archaeological_warehouse/migrations/0126_add_timezone_django_v4.py index 41cda4e35..d4fd3eee0 100644 --- a/archaeological_warehouse/migrations/0126_add_timezone_django_v4.py +++ b/archaeological_warehouse/migrations/0126_add_timezone_django_v4.py @@ -10,7 +10,7 @@ import re class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0267_add_timezone_django_v4'), + ('ishtar_common', '0268_add_timezone_django_v4'), ('archaeological_warehouse', '0125_migrate_cached_town'), ] diff --git a/ishtar_common/management/commands/process_initialize_item_keys.py b/ishtar_common/management/commands/process_initialize_item_keys.py index e493066f1..000a526d9 100644 --- a/ishtar_common/management/commands/process_initialize_item_keys.py +++ b/ishtar_common/management/commands/process_initialize_item_keys.py @@ -1,27 +1,31 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import datetime import sys from django.apps import apps +from django.db import connection from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand from django.utils.text import slugify from ishtar_common.models import ItemKey -from ishtar_common.utils import get_log_time, get_percent, get_eta, BColors +from ishtar_common.utils import get_log_time, get_percent, BColors -def write_output(base_lbl, idx, total, ref_time): +def write_output(base_lbl, idx, total): lbl = f"\r{BColors.OKBLUE}[{get_percent(idx, total)}] {base_lbl} {idx + 1}/{total}" - lbl += f" ({get_eta(idx, total, ref_time, datetime.datetime.now())} left)" - lbl += "{BColors.ENDC}" + lbl += f"{BColors.ENDC}" sys.stdout.write(lbl) sys.stdout.flush() +SQL_CHECK_DATABASE_EXISTS = """SELECT 1 FROM information_schema.tables + WHERE table_type='BASE TABLE' AND table_schema='public' + AND table_catalog='{database}' AND table_name='{table_name}';""" + + def migrate_item_key(clean_old=False, quiet=False): """ clean_old=False: set to True to migrate from non unicode to unicode @@ -31,26 +35,39 @@ def migrate_item_key(clean_old=False, quiet=False): "archaeological_finds", "archaeological_operations", "archaeological_warehouse"): app_models = apps.get_app_config(app).get_models() + is_init = False for model in app_models: + if is_init: # no need to initialize + break if any(1 for attr in ("available", "txt_idx", "comment", "available") if not hasattr(model, attr)): continue # not a general type content_type, __ = ContentType.objects.get_or_create( app_label=app, model=model._meta.model_name) - ref_time = datetime.datetime.now() + sql = SQL_CHECK_DATABASE_EXISTS.format( + database=settings.DATABASES["default"]["NAME"], + table_name=model._meta.db_table) + res = None + with connection.cursor() as cursor: + cursor.execute(sql) + res = cursor.fetchall() + if not res: # table is not created yet (database initialisation or migration) + is_init = True + break q = model.objects nb = q.count() - for idx, item in enumerate(q.all()): + for idx, item in enumerate(q.values_list("id", "label")): + pk, label = item if not quiet: - write_output(model._meta.verbose_name, idx, nb, ref_time) + write_output(model._meta.verbose_name, idx, nb) if clean_old: ItemKey.objects.filter( - key=slugify(item.label), + key=slugify(label), content_type=content_type, importer_type=None, ishtar_import=None, user=None, group=None).delete() ItemKey.objects.get_or_create( - key=slugify(item.label, allow_unicode=True), - content_type=content_type, object_id=item.pk, + key=slugify(label, allow_unicode=True), + content_type=content_type, object_id=pk, importer_type=None, ishtar_import=None, user=None, group=None) lbl = f"\r{BColors.OKGREEN}* {model._meta.verbose_name} - OK{SPACE}{BColors.ENDC}\n" diff --git a/ishtar_common/migrations/0267_add_timezone_django_v4.py b/ishtar_common/migrations/0268_add_timezone_django_v4.py index 0eb973d87..f12359691 100644 --- a/ishtar_common/migrations/0267_add_timezone_django_v4.py +++ b/ishtar_common/migrations/0268_add_timezone_django_v4.py @@ -10,7 +10,7 @@ import re class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0266_migrate_cached_town'), + ('ishtar_common', '0267_reinitialize_item_keys'), ] operations = [ diff --git a/ishtar_common/migrations/0268_userrequesttoken_usertoken.py b/ishtar_common/migrations/0268_userrequesttoken_usertoken.py deleted file mode 100644 index 9c13acc64..000000000 --- a/ishtar_common/migrations/0268_userrequesttoken_usertoken.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 4.2.19 on 2025-05-09 08:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ishtar_common', '0267_add_timezone_django_v4'), - ] - - operations = [ - migrations.CreateModel( - name='UserRequestToken', - fields=[ - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), - ('key', models.CharField(max_length=6, unique=True, verbose_name='Key')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='user_request_token', serialize=False, to=settings.AUTH_USER_MODEL, verbose_name='User')), - ('access_type', models.CharField(choices=[('R', 'GIS - read'), ('W', 'GIS - read/write')], default='R', max_length=1, verbose_name='Access type')), - ('name', models.TextField(verbose_name='Name')), - ('limit_date', models.DateField(blank=True, null=True, verbose_name='Limit date')), - ], - options={ - 'verbose_name': 'API - GIS - Request token', - 'verbose_name_plural': 'API - GIS - Request tokens', - }, - ), - migrations.AlterModelOptions( - name='importertype', - options={'ordering': ('name',), 'permissions': (('view_gis_importer', 'Can export to QGIS'), ('view_own_gis_importer', 'Can export own to QGIS'), ('change_gis_importer', 'Can import from QGIS'), ('change_own_gis_importer', 'Can import own to QGIS')), 'verbose_name': 'Importer - Type', 'verbose_name_plural': 'Importer - Types'}, - ), - migrations.AddField( - model_name='ishtarsiteprofile', - name='gis_connector', - field=models.BooleanField(default=False, verbose_name='GIS connector'), - ), - migrations.AlterField( - model_name='importertype', - name='type', - field=models.CharField(choices=[('tab', 'Table'), ('gis', 'GIS'), ('qgs', 'QGIS')], default='tab', max_length=3, verbose_name='Type'), - ), - migrations.CreateModel( - name='UserToken', - fields=[ - ('key', models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name='Key')), - ('name', models.TextField(verbose_name='Name')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), - ('access_type', models.CharField(choices=[('R', 'GIS - read'), ('W', 'GIS - read/write')], default='R', max_length=1, verbose_name='Access type')), - ('limit_date', models.DateField(blank=True, null=True, verbose_name='Limit date')), - ('last_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Last IP')), - ('last_access', models.DateField(auto_now_add=True, verbose_name='Last access date')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_token', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'API - GIS - Token', - 'verbose_name_plural': 'API - GIS - Tokens', - 'ordering': ('user', 'name', 'limit_date'), - }, - ), - ] diff --git a/ishtar_common/migrations/0269_gis_importchunk.py b/ishtar_common/migrations/0269_gis_importchunk.py deleted file mode 100644 index 995a8642d..000000000 --- a/ishtar_common/migrations/0269_gis_importchunk.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 4.2.19 on 2025-07-22 08:21 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0268_userrequesttoken_usertoken'), - ] - - operations = [ - migrations.CreateModel( - name='ImportChunk', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('send_datetime', models.DateTimeField(verbose_name='Send date')), - ('chunk', models.TextField(verbose_name='Chunk')), - ('number', models.IntegerField(verbose_name='Number')), - ('total', models.IntegerField(verbose_name='Total')), - ('import_immediatly', models.BooleanField(default=False, verbose_name='Import Immediatly')), - ('importer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.importertype', verbose_name='Importer')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.ishtaruser', verbose_name='User')), - ], - options={ - 'verbose_name': 'Import - Chunk', - 'verbose_name_plural': 'Import - Chunks', - }, - ), - ] diff --git a/ishtar_common/migrations/0269_gis_plugin.py b/ishtar_common/migrations/0269_gis_plugin.py new file mode 100644 index 000000000..864183027 --- /dev/null +++ b/ishtar_common/migrations/0269_gis_plugin.py @@ -0,0 +1,137 @@ +# Generated by Django 4.2.19 on 2026-02-25 09:44 + +import sys +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from django.utils.translation import gettext_lazy as _ + +from ishtar_common.utils_migrations import update_import_key, print_progress + + +def update_importkey(apps, __): + updated = 0 + GeoVectorData = apps.get_model("ishtar_common", "geovectordata") + total = GeoVectorData.objects.count() + sys.stdout.write("\n") + for idx, data in enumerate(GeoVectorData.objects.all()): + print_progress(idx, total) + updated += 1 if update_import_key(data) else 0 + + +def update_groups(apps, __): + Permission = apps.get_model("auth", "Permission") + Group = apps.get_model("auth", "Group") + ContentType = apps.get_model("contenttypes", "ContentType") + ct = ContentType.objects.get(app_label='ishtar_common', model='import') + update = ( + ("Imports : lecture", "view_gis_import", _("Can export to QGIS")), + ("Imports rattachés : lecture", "view_own_gis_import", _("Can export own to QGIS")), + ("Imports : ajout", "change_gis_import", _("Can create import from QGIS")), + ) + for gp_name, codename, p_name in update: + gp = Group.objects.filter(name=gp_name) + if gp.exists(): + perm = Permission.objects.filter(codename=codename) + if perm.exists(): + perm = perm.all()[0] + else: + perm = Permission.objects.create( + codename=codename, + name=p_name, + content_type=ct + ) + + gp = gp.all()[0] + gp.permissions.add(perm) + + +class Migration(migrations.Migration): + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('ishtar_common', '0268_add_timezone_django_v4'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserRequestToken', + fields=[ + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('key', models.CharField(max_length=6, unique=True, verbose_name='Key')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='user_request_token', serialize=False, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('access_type', models.CharField(choices=[('R', 'GIS - read'), ('W', 'GIS - read/write')], default='R', max_length=1, verbose_name='Access type')), + ('name', models.TextField(verbose_name='Name')), + ('limit_date', models.DateField(blank=True, null=True, verbose_name='Limit date')), + ], + options={ + 'verbose_name': 'API - GIS - Request token', + 'verbose_name_plural': 'API - GIS - Request tokens', + }, + ), + migrations.AlterModelOptions( + name='importertype', + options={'ordering': ('name',), 'permissions': (('view_gis_importer', 'Can export to QGIS'), ('view_own_gis_importer', 'Can export own to QGIS'), ('change_gis_importer', 'Can import from QGIS'), ('change_own_gis_importer', 'Can import own to QGIS')), 'verbose_name': 'Importer - Type', 'verbose_name_plural': 'Importer - Types'}, + ), + migrations.AddField( + model_name='ishtarsiteprofile', + name='gis_connector', + field=models.BooleanField(default=False, verbose_name='GIS connector'), + ), + migrations.AlterField( + model_name='importertype', + name='type', + field=models.CharField(choices=[('tab', 'Table'), ('gis', 'GIS'), ('qgs', 'QGIS')], default='tab', max_length=3, verbose_name='Type'), + ), + migrations.CreateModel( + name='UserToken', + fields=[ + ('key', models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name='Key')), + ('name', models.TextField(verbose_name='Name')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('access_type', models.CharField(choices=[('R', 'GIS - read'), ('W', 'GIS - read/write')], default='R', max_length=1, verbose_name='Access type')), + ('limit_date', models.DateField(blank=True, null=True, verbose_name='Limit date')), + ('last_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Last IP')), + ('last_access', models.DateField(auto_now_add=True, verbose_name='Last access date')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_token', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'API - GIS - Token', + 'verbose_name_plural': 'API - GIS - Tokens', + 'ordering': ('user', 'name', 'limit_date'), + }, + ), + migrations.CreateModel( + name='ImportChunk', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('send_datetime', models.DateTimeField(verbose_name='Send date')), + ('chunk', models.TextField(verbose_name='Chunk')), + ('number', models.IntegerField(verbose_name='Number')), + ('total', models.IntegerField(verbose_name='Total')), + ('import_immediatly', models.BooleanField(default=False, verbose_name='Import Immediatly')), + ('importer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.importertype', verbose_name='Importer')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.ishtaruser', verbose_name='User')), + ], + options={ + 'verbose_name': 'Import - Chunk', + 'verbose_name_plural': 'Import - Chunks', + }, + ), + migrations.RunPython(update_importkey), + migrations.AddField( + model_name='import', + name='import_immediatly', + field=models.BooleanField(default=False, verbose_name='Import Immediatly'), + ), + migrations.AddField( + model_name='importertype', + name='gis_type', + field=models.ForeignKey(blank=True, help_text='For QGIS importer type. Geographic data type used for import and export.', null=True, on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.geodatatype'), + ), + migrations.AlterModelOptions( + name='import', + options={'permissions': (('view_own_import', 'Can view own Import'), ('add_own_import', 'Can add own Import'), ('change_own_import', 'Can change own Import'), ('delete_own_import', 'Can delete own Import'), ('view_gis_import', 'Can export to QGIS'), ('view_own_gis_import', 'Can export own to QGIS'), ('change_gis_import', 'Can import from QGIS'), ('change_own_gis_import', 'Can import own to QGIS')), 'verbose_name': 'Import - Import', 'verbose_name_plural': 'Import - Imports'}, + ), + migrations.RunPython(update_groups) + ] diff --git a/ishtar_common/migrations/0270_gis_import_key_init.py b/ishtar_common/migrations/0270_gis_import_key_init.py deleted file mode 100644 index 030db3a25..000000000 --- a/ishtar_common/migrations/0270_gis_import_key_init.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys -from django.db import migrations - -from ishtar_common.utils_migrations import update_import_key, print_progress - - -def update_importkey(apps, __): - updated = 0 - GeoVectorData = apps.get_model("ishtar_common", "geovectordata") - total = GeoVectorData.objects.count() - sys.stdout.write("\n") - for idx, data in enumerate(GeoVectorData.objects.all()): - print_progress(idx, total) - updated += 1 if update_import_key(data) else 0 - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0269_gis_importchunk'), - ] - - operations = [ - migrations.RunPython(update_importkey) - ] diff --git a/ishtar_common/migrations/0272_ishtarsiteprofile_dating_external_id.py b/ishtar_common/migrations/0270_ishtarsiteprofile_dating_external_id.py index a3c8f278a..fb6ac3944 100644 --- a/ishtar_common/migrations/0272_ishtarsiteprofile_dating_external_id.py +++ b/ishtar_common/migrations/0270_ishtarsiteprofile_dating_external_id.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0271_import_import_immediatly'), + ('ishtar_common', '0269_gis_plugin'), ] operations = [ diff --git a/ishtar_common/migrations/0271_import_import_immediatly.py b/ishtar_common/migrations/0271_import_import_immediatly.py deleted file mode 100644 index ff94a566b..000000000 --- a/ishtar_common/migrations/0271_import_import_immediatly.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.19 on 2025-08-05 09:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0270_gis_import_key_init'), - ] - - operations = [ - migrations.AddField( - model_name='import', - name='import_immediatly', - field=models.BooleanField(default=False, verbose_name='Import Immediatly'), - ), - ] diff --git a/ishtar_common/migrations/0275_importerduplicatefield_concat_str.py b/ishtar_common/migrations/0271_importerduplicatefield_concat_str.py index be535d629..3d4a3badf 100644 --- a/ishtar_common/migrations/0275_importerduplicatefield_concat_str.py +++ b/ishtar_common/migrations/0271_importerduplicatefield_concat_str.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('ishtar_common', '0274_import_gis_options'), + ('ishtar_common', '0270_ishtarsiteprofile_dating_external_id'), ] operations = [ diff --git a/ishtar_common/migrations/0273_importertype_gis_type.py b/ishtar_common/migrations/0273_importertype_gis_type.py deleted file mode 100644 index 3438b76b9..000000000 --- a/ishtar_common/migrations/0273_importertype_gis_type.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.19 on 2025-12-15 10:25 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0272_ishtarsiteprofile_dating_external_id'), - ] - - operations = [ - migrations.AddField( - model_name='importertype', - name='gis_type', - field=models.ForeignKey(blank=True, help_text='For QGIS importer type. Geographic data type used for import and export.', null=True, on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.geodatatype'), - ), - ] diff --git a/ishtar_common/migrations/0274_import_gis_options.py b/ishtar_common/migrations/0274_import_gis_options.py deleted file mode 100644 index 553f2fb83..000000000 --- a/ishtar_common/migrations/0274_import_gis_options.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.19 on 2026-01-21 14:08 - -from django.db import migrations -from django.utils.translation import gettext_lazy as _ - - -def update_groups(apps, __): - Permission = apps.get_model("auth", "Permission") - Group = apps.get_model("auth", "Group") - ContentType = apps.get_model("contenttypes", "ContentType") - ct = ContentType.objects.get(app_label='ishtar_common', model='import') - update = ( - ("Imports : lecture", "view_gis_import", _("Can export to QGIS")), - ("Imports rattachés : lecture", "view_own_gis_import", _("Can export own to QGIS")), - ("Imports : ajout", "change_gis_import", _("Can create import from QGIS")), - ) - for gp_name, codename, p_name in update: - gp = Group.objects.filter(name=gp_name) - if gp.exists(): - perm = Permission.objects.filter(codename=codename) - if perm.exists(): - perm = perm.all()[0] - else: - perm = Permission.objects.create( - codename=codename, - name=p_name, - content_type=ct - ) - - gp = gp.all()[0] - gp.permissions.add(perm) - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0273_importertype_gis_type'), - ] - - operations = [ - migrations.AlterModelOptions( - name='import', - options={'permissions': (('view_own_import', 'Can view own Import'), ('add_own_import', 'Can add own Import'), ('change_own_import', 'Can change own Import'), ('delete_own_import', 'Can delete own Import'), ('view_gis_import', 'Can export to QGIS'), ('view_own_gis_import', 'Can export own to QGIS'), ('change_gis_import', 'Can import from QGIS'), ('change_own_gis_import', 'Can import own to QGIS')), 'verbose_name': 'Import - Import', 'verbose_name_plural': 'Import - Imports'}, - ), - migrations.RunPython(update_groups) - ] |
