From e54497c09f4326deac41176985379ae423d1ee2e Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Thu, 9 Jan 2025 10:58:41 +0200 Subject: [PATCH] Add location model and location field to org --- ...ias_location_locations_by_name_and_more.py | 108 ++++++++++++++++++ temba/locations/models.py | 43 ++++++- temba/orgs/migrations/0164_remove_viewers.py | 2 +- temba/orgs/migrations/0165_org_location.py | 20 ++++ temba/orgs/models.py | 1 + temba/orgs/tests/test_migrations.py | 10 -- 6 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 temba/locations/migrations/0033_location_locationalias_location_locations_by_name_and_more.py create mode 100644 temba/orgs/migrations/0165_org_location.py delete mode 100644 temba/orgs/tests/test_migrations.py diff --git a/temba/locations/migrations/0033_location_locationalias_location_locations_by_name_and_more.py b/temba/locations/migrations/0033_location_locationalias_location_locations_by_name_and_more.py new file mode 100644 index 00000000000..8846312fd1f --- /dev/null +++ b/temba/locations/migrations/0033_location_locationalias_location_locations_by_name_and_more.py @@ -0,0 +1,108 @@ +# Generated by Django 5.1.4 on 2025-01-10 10:06 + +import mptt.fields + +import django.db.models.deletion +import django.db.models.functions.text +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("locations", "0032_squashed"), + ("orgs", "0164_remove_viewers"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Location", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("osm_id", models.CharField(max_length=15, unique=True)), + ("name", models.CharField(max_length=128)), + ("level", models.IntegerField()), + ("path", models.CharField(max_length=768)), + ("geometry", models.JSONField(null=True)), + ("lft", models.PositiveIntegerField(editable=False)), + ("rght", models.PositiveIntegerField(editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "parent", + mptt.fields.TreeForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="children", + to="locations.location", + ), + ), + ], + ), + migrations.CreateModel( + name="LocationAlias", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "is_active", + models.BooleanField( + default=True, help_text="Whether this item is active, use this instead of deleting" + ), + ), + ( + "created_on", + models.DateTimeField( + blank=True, + default=django.utils.timezone.now, + editable=False, + help_text="When this item was originally created", + ), + ), + ( + "modified_on", + models.DateTimeField( + blank=True, + default=django.utils.timezone.now, + editable=False, + help_text="When this item was last modified", + ), + ), + ("name", models.CharField(help_text="The name for our alias", max_length=128)), + ( + "created_by", + models.ForeignKey( + help_text="The user which originally created this item", + on_delete=django.db.models.deletion.PROTECT, + related_name="%(app_label)s_%(class)s_creations", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "location", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, related_name="aliases", to="locations.location" + ), + ), + ( + "modified_by", + models.ForeignKey( + help_text="The user which last modified this item", + on_delete=django.db.models.deletion.PROTECT, + related_name="%(app_label)s_%(class)s_modifications", + to=settings.AUTH_USER_MODEL, + ), + ), + ("org", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="orgs.org")), + ], + ), + migrations.AddIndex( + model_name="location", + index=models.Index(django.db.models.functions.text.Lower("name"), name="locations_by_name"), + ), + migrations.AddIndex( + model_name="locationalias", + index=models.Index(django.db.models.functions.text.Lower("name"), name="locationaliases_by_name"), + ), + ] diff --git a/temba/locations/models.py b/temba/locations/models.py index bc93d9935c5..6ef28743e36 100644 --- a/temba/locations/models.py +++ b/temba/locations/models.py @@ -4,7 +4,7 @@ from django.contrib.gis.db import models from django.db.models import F, Value -from django.db.models.functions import Concat, Upper +from django.db.models.functions import Concat, Lower, Upper # default manager for AdminBoundary, doesn't load geometries @@ -178,3 +178,44 @@ def create(cls, org, user, boundary, name): class Meta: indexes = [models.Index(Upper("name"), name="boundaryaliases_by_name")] + + +class Location(MPTTModel, models.Model): + """ + Represents a single administrative locations (like a country, state or district) + """ + + LEVEL_COUNTRY = 0 + LEVEL_STATE = 1 + LEVEL_DISTRICT = 2 + LEVEL_WARD = 3 + + MAX_NAME_LEN = 128 + + # Used to separate segments in a hierarchy of locations. Has the advantage of being a character in GSM7 and + # being very unlikely to show up in an admin region name. + PATH_SEPARATOR = ">" + PADDED_PATH_SEPARATOR = " > " + + osm_id = models.CharField(max_length=15, unique=True) + name = models.CharField(max_length=MAX_NAME_LEN) + level = models.IntegerField() + parent = TreeForeignKey("self", null=True, on_delete=models.PROTECT, related_name="children", db_index=True) + path = models.CharField(max_length=768) # e.g. Rwanda > Kigali + geometry = models.JSONField(null=True) + + class Meta: + indexes = [models.Index(Lower("name"), name="locations_by_name")] + + +class LocationAlias(SmartModel): + """ + An org specific alias for a location name + """ + + org = models.ForeignKey("orgs.Org", on_delete=models.PROTECT) + location = models.ForeignKey(Location, on_delete=models.PROTECT, related_name="aliases") + name = models.CharField(max_length=Location.MAX_NAME_LEN, help_text="The name for our alias") + + class Meta: + indexes = [models.Index(Lower("name"), name="locationaliases_by_name")] diff --git a/temba/orgs/migrations/0164_remove_viewers.py b/temba/orgs/migrations/0164_remove_viewers.py index fb14da4fa6a..ce5709122d6 100644 --- a/temba/orgs/migrations/0164_remove_viewers.py +++ b/temba/orgs/migrations/0164_remove_viewers.py @@ -3,7 +3,7 @@ from django.db import migrations -def remove_viewers(apps, schema_editor): +def remove_viewers(apps, schema_editor): # pragma: no cover OrgMembership = apps.get_model("orgs", "OrgMembership") num_deleted = OrgMembership.objects.filter(role_code="V").delete()[0] if num_deleted: diff --git a/temba/orgs/migrations/0165_org_location.py b/temba/orgs/migrations/0165_org_location.py new file mode 100644 index 00000000000..a87cd3e792e --- /dev/null +++ b/temba/orgs/migrations/0165_org_location.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.4 on 2025-01-10 10:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("locations", "0033_location_locationalias_location_locations_by_name_and_more"), + ("orgs", "0164_remove_viewers"), + ] + + operations = [ + migrations.AddField( + model_name="org", + name="location", + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to="locations.location"), + ), + ] diff --git a/temba/orgs/models.py b/temba/orgs/models.py index 7f18df6887e..869b45a3a1e 100644 --- a/temba/orgs/models.py +++ b/temba/orgs/models.py @@ -507,6 +507,7 @@ class Org(SmartModel): help_text=_("Default formatting and parsing of dates in flows and messages."), ) country = models.ForeignKey("locations.AdminBoundary", null=True, on_delete=models.PROTECT) + location = models.ForeignKey("locations.Location", null=True, on_delete=models.PROTECT) flow_languages = ArrayField(models.CharField(max_length=3), default=list, validators=[ArrayMinLengthValidator(1)]) input_collation = models.CharField(max_length=32, choices=COLLATION_CHOICES, default=COLLATION_DEFAULT) flow_smtp = models.CharField(null=True) # e.g. smtp://... diff --git a/temba/orgs/tests/test_migrations.py b/temba/orgs/tests/test_migrations.py deleted file mode 100644 index 4f93479d97e..00000000000 --- a/temba/orgs/tests/test_migrations.py +++ /dev/null @@ -1,10 +0,0 @@ -from temba.tests import MigrationTest - - -class RemoveViewersTest(MigrationTest): - app = "orgs" - migrate_from = "0163_squashed" - migrate_to = "0164_remove_viewers" - - def test_migration(self): - self.assertEqual({self.admin, self.editor, self.agent}, set(self.org.get_users()))