diff --git a/arches/app/models/functions.py b/arches/app/models/functions.py new file mode 100644 index 00000000000..0b9444eb613 --- /dev/null +++ b/arches/app/models/functions.py @@ -0,0 +1,7 @@ +from django.db import models + + +class UUID4(models.Func): + function = "uuid_generate_v4" + arity = 0 + output_field = models.UUIDField() diff --git a/arches/app/models/migrations/0001_initial.py b/arches/app/models/migrations/0001_initial.py index 2b014778110..3e5d32fcc30 100644 --- a/arches/app/models/migrations/0001_initial.py +++ b/arches/app/models/migrations/0001_initial.py @@ -1,19 +1,12 @@ -# -*- coding: utf-8 -*- - - +import codecs import os import uuid -import codecs -import django.contrib.gis.db.models.fields -from django.core import management + from django.contrib.postgres.fields import JSONField from django.db import migrations, models -from arches.db.migration_operations.extras import ( - CreateExtension, - CreateAutoPopulateUUIDField, - CreateFunction, -) + from arches.app.models.system_settings import settings +from arches.db.migration_operations.extras import CreateExtension, CreateFunction def get_sql_string_from_file(pathtofile): @@ -108,6 +101,14 @@ def make_permissions(apps, schema_editor, with_create_permissions=True): admin_user.groups.add(guest_group) +# For historical purposes only. +# UUID4 exists in arches.app.models.functions for general use. +class UUID1(models.Func): + function = "uuid_generate_v1" + arity = 0 + output_field = models.UUIDField() + + class Migration(migrations.Migration): dependencies = [ @@ -201,7 +202,9 @@ class Migration(migrations.Migration): ( "graphid", models.UUIDField( - default=uuid.uuid1, serialize=False, primary_key=True + db_default=UUID1(), + serialize=False, + primary_key=True, ), ), ("name", models.TextField(null=True, blank=True)), @@ -237,7 +240,9 @@ class Migration(migrations.Migration): ( "cardid", models.UUIDField( - default=uuid.uuid1, serialize=False, primary_key=True + db_default=UUID1(), + serialize=False, + primary_key=True, ), ), ("name", models.TextField(null=True, blank=True)), @@ -295,7 +300,9 @@ class Migration(migrations.Migration): ( "conceptid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("legacyoid", models.TextField(unique=True)), @@ -380,7 +387,9 @@ class Migration(migrations.Migration): ( "edgeid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("name", models.TextField(blank=True, null=True)), @@ -408,7 +417,9 @@ class Migration(migrations.Migration): ( "editlogid", models.UUIDField( - default=uuid.uuid1, serialize=False, primary_key=True + db_default=UUID1(), + serialize=False, + primary_key=True, ), ), ("resourceclassid", models.TextField(null=True, blank=True)), @@ -452,7 +463,9 @@ class Migration(migrations.Migration): ( "formid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("title", models.TextField(blank=True, null=True)), @@ -571,7 +584,9 @@ class Migration(migrations.Migration): ( "nodeid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("name", models.TextField()), @@ -602,7 +617,9 @@ class Migration(migrations.Migration): ( "nodegroupid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("legacygroupid", models.TextField(blank=True, null=True)), @@ -684,7 +701,9 @@ class Migration(migrations.Migration): ( "relationid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ( @@ -786,7 +805,9 @@ class Migration(migrations.Migration): ( "resource2resourceid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ( @@ -823,7 +844,9 @@ class Migration(migrations.Migration): ( "resourceinstanceid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("legacyid", models.TextField(blank=True, unique=True, null=True)), @@ -866,7 +889,9 @@ class Migration(migrations.Migration): ( "tileid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("data", JSONField(blank=True, db_column="tiledata", null=True)), @@ -909,7 +934,9 @@ class Migration(migrations.Migration): ( "valueid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("value", models.TextField()), @@ -951,7 +978,9 @@ class Migration(migrations.Migration): ( "widgetid", models.UUIDField( - default=uuid.uuid1, primary_key=True, serialize=False + db_default=UUID1(), + primary_key=True, + serialize=False, ), ), ("name", models.TextField()), @@ -1224,22 +1253,6 @@ class Migration(migrations.Migration): name="functionxgraph", unique_together={("function", "graph")}, ), - CreateAutoPopulateUUIDField("graphs", ["graphid"]), - CreateAutoPopulateUUIDField("cards", ["cardid"]), - CreateAutoPopulateUUIDField("concepts", ["conceptid"]), - CreateAutoPopulateUUIDField("edges", ["edgeid"]), - CreateAutoPopulateUUIDField("edit_log", ["editlogid"]), - CreateAutoPopulateUUIDField("forms", ["formid"]), - CreateAutoPopulateUUIDField("node_groups", ["nodegroupid"]), - CreateAutoPopulateUUIDField("nodes", ["nodeid"]), - CreateAutoPopulateUUIDField("relations", ["relationid"]), - CreateAutoPopulateUUIDField( - "resource_2_resource_constraints", ["resource2resourceid"] - ), - CreateAutoPopulateUUIDField("resource_instances", ["resourceinstanceid"]), - CreateAutoPopulateUUIDField("tiles", ["tileid"]), - CreateAutoPopulateUUIDField("values", ["valueid"]), - CreateAutoPopulateUUIDField("widgets", ["widgetid"]), migrations.RunSQL( """ ALTER TABLE nodes ADD CONSTRAINT nodes_ddatatypes_fk FOREIGN KEY (datatype) diff --git a/arches/app/models/migrations/10957_refactor_relations_rule.py b/arches/app/models/migrations/10957_refactor_relations_rule.py new file mode 100644 index 00000000000..a1dc0af0f7f --- /dev/null +++ b/arches/app/models/migrations/10957_refactor_relations_rule.py @@ -0,0 +1,110 @@ +# Generated by Django 5.2a1 on 2025-02-12 10:28 + +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("models", "11725_make_tiledata_not_nullable"), + ] + + operations = [ + migrations.RunSQL( + """ + DROP RULE relations_check_insert ON relations; + DROP RULE relations_check_update ON relations; + + DROP FUNCTION public.__arches_check_dup_relations( + p_conceptid1 uuid, + p_conceptid2 uuid, + p_relationtype text); + """, + """ + CREATE OR REPLACE FUNCTION public.__arches_check_dup_relations( + p_conceptid1 uuid, + p_conceptid2 uuid, + p_relationtype text) + RETURNS text + LANGUAGE 'plpgsql' + + COST 100 + VOLATILE + + AS $BODY$ + + declare + v_return text; + + BEGIN + IF + ( SELECT count(*) from relations + WHERE 1=1 + AND conceptidfrom = p_conceptid1 + AND conceptidto = p_conceptid2 + AND relationtype = p_relationtype ) > 0 + THEN v_return = 'duplicate'; + + ELSIF + ( SELECT count(*) from relations + WHERE 1=1 + AND conceptidfrom = p_conceptid2 + AND conceptidto = p_conceptid1 + AND relationtype = p_relationtype ) > 0 + THEN v_return = 'duplicate'; + + ELSE v_return = 'unique'; + + END IF; + + RETURN v_return; + + END; + + $BODY$; + + + CREATE OR REPLACE RULE relations_check_insert AS ON INSERT TO relations + WHERE (select * from __arches_check_dup_relations(new.conceptidfrom,new.conceptidto,new.relationtype)) = 'duplicate' + DO INSTEAD NOTHING; + + CREATE OR REPLACE RULE relations_check_update AS ON UPDATE TO relations + WHERE (select * from __arches_check_dup_relations(new.conceptidfrom,new.conceptidto,new.relationtype)) = 'duplicate' + DO INSTEAD NOTHING; + """, + ), + migrations.AlterUniqueTogether( + name="relation", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="relation", + constraint=models.UniqueConstraint( + models.Case( + models.When( + django.db.models.expressions.CombinedExpression( + models.F("conceptfrom"), + "<", + models.F("conceptto"), + output_field=models.BooleanField(), + ), + then=django.db.models.functions.text.Concat( + models.F("conceptfrom"), + models.Value(","), + models.F("conceptto"), + output_field=models.TextField(), + ), + ), + default=django.db.models.functions.text.Concat( + models.F("conceptto"), + models.Value(","), + models.F("conceptfrom"), + output_field=models.TextField(), + ), + ), + models.F("relationtype"), + name="unique_relation_bidirectional", + ), + ), + ] diff --git a/arches/app/models/migrations/10958_restore_pk_defaults.py b/arches/app/models/migrations/10958_restore_pk_defaults.py new file mode 100644 index 00000000000..6023ae47256 --- /dev/null +++ b/arches/app/models/migrations/10958_restore_pk_defaults.py @@ -0,0 +1,397 @@ +# Generated by Django 5.2a1 on 2025-02-18 12:21 + +import arches.app.models.functions +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("models", "10957_refactor_relations_rule"), + ] + + operations = [ + migrations.AlterField( + model_name="cardcomponent", + name="componentid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="cardmodel", + name="cardid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="cardxnodexwidget", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="concept", + name="conceptid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="constraintmodel", + name="constraintid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="constraintxnode", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="edge", + name="edgeid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="editlog", + name="editlogid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="externaloauthtoken", + name="token_id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="file", + name="fileid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="function", + name="functionid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="functionxgraph", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="geocoder", + name="geocoderid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="graphmodel", + name="graphid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="graphxmapping", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="maplayer", + name="maplayerid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="node", + name="nodeid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="nodegroup", + name="nodegroupid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="notification", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="notificationtype", + name="typeid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="ontology", + name="ontologyid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="ontologyclass", + name="ontologyclassid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="plugin", + name="pluginid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="relation", + name="relationid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="reporttemplate", + name="templateid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="resource2resourceconstraint", + name="resource2resourceid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="resourceinstance", + name="resourceinstanceid", + field=models.UUIDField( + blank=True, + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="resourcerevisionlog", + name="logid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="resourcexresource", + name="resourcexid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="searchcomponent", + name="searchcomponentid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="searchexporthistory", + name="searchexportid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="tempfile", + name="fileid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="tilemodel", + name="tileid", + field=models.UUIDField( + blank=True, + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="userxnotification", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="userxnotificationtype", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="userxtask", + name="id", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="value", + name="valueid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + migrations.AlterField( + model_name="widget", + name="widgetid", + field=models.UUIDField( + db_default=arches.app.models.functions.UUID4(), + default=uuid.uuid4, + primary_key=True, + serialize=False, + ), + ), + ] diff --git a/arches/app/models/migrations/10959_update_uuid_defaults_to_v4.py b/arches/app/models/migrations/10959_update_uuid_defaults_to_v4.py new file mode 100644 index 00000000000..5c08a86a75e --- /dev/null +++ b/arches/app/models/migrations/10959_update_uuid_defaults_to_v4.py @@ -0,0 +1,52 @@ +# Generated by Django 5.2a1 on 2025-02-18 13:58 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("models", "10958_restore_pk_defaults"), + ] + + operations = [ + migrations.AlterField( + model_name="editlog", + name="transactionid", + field=models.UUIDField(default=uuid.uuid4), + ), + migrations.AlterField( + model_name="etlmodule", + name="etlmoduleid", + field=models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="graphxpublishedgraph", + name="publicationid", + field=models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="publishedgraphedit", + name="edit_id", + field=models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + migrations.AlterField( + model_name="resourcerevisionlog", + name="resourceid", + field=models.UUIDField(default=uuid.uuid4), + ), + migrations.AlterField( + model_name="spatialview", + name="spatialviewid", + field=models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ] diff --git a/arches/app/models/mixins.py b/arches/app/models/mixins.py new file mode 100644 index 00000000000..d020bb397ff --- /dev/null +++ b/arches/app/models/mixins.py @@ -0,0 +1,45 @@ +class SaveSupportsBlindOverwriteMixin: + def add_force_keyword(self, kwargs): + """ + Django 3.0 introduced a performance optimization when calling save() on + instances where its primary key field has a default. In this case, Django + assumes that calling save() on an instance that has not been fetched + will never be used to overwrite an existing row. For "blind" overwrites, + the suggested pattern is to use queryset methods update() or update_or_create() + to signal that overwriting is intentional. In Django 5.1+, you can at least + pass force_update=True to save() to perform a blind overwrite if desired. + This helper determines whether that's needed and sets force_update=True if so. + + **New Django models should avoid using this mixin** and should use update() + or update_or_create() instead. This is really just here to avoiding auditing + for blind overwrites in code calling save() on models predating Django 3.0. + + (The solution in Arches 6.2 during the Django 3.2 upgrade was just to remove + field defaults, in favor of overrides of __init__() and save() to supply defaults, + but that left gaps, e.g. queryset methods like create(). So Arches 8.0 added + the field defaults back and added this mixin.) + + https://forum.djangoproject.com/t/save-behavior-when-updating-model-with-default-primary-keys + """ + new_kwargs = {**kwargs} + if new_kwargs.get("force_insert") or new_kwargs.get("force_update"): + # The caller knows what they are doing. + return new_kwargs + has_default_pk = all( + f.has_default() or f.has_db_default() for f in self._meta.pk_fields + ) + if not has_default_pk: + msg = f"Calling add_force_keyword() is a pessimization for models without default primary keys: {self.__class__.__name__}" + raise ValueError(msg) + row_exists = ( + self.__class__.objects.using(new_kwargs.get("using")) + .filter(pk=self.pk) + .exists() + ) + if row_exists: + new_kwargs["force_update"] = True + return new_kwargs + + def save(self, **kwargs): + kwargs = self.add_force_keyword(kwargs) + super().save(**kwargs) diff --git a/arches/app/models/models.py b/arches/app/models/models.py index d07ef59b7f1..832cd3efcdb 100644 --- a/arches/app/models/models.py +++ b/arches/app/models/models.py @@ -1,46 +1,33 @@ -# This is an auto-generated Django model module. -# You'll have to do the following manually to clean this up: -# * Rearrange models' order -# * Make sure each model has one field with primary_key=True -# * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table -# Feel free to rename the models, but don't rename db_table values or field names. -# -# Also note: You'll have to insert the output of 'django-admin sqlcustom [app_label]' -# into your database. - - -import sys -import json -import uuid import datetime +import json import logging +import sys import traceback -import django.utils.timezone +import uuid -from arches.app.const import ExtensionType -from arches.app.utils.module_importer import get_class_from_modulename -from arches.app.utils.thumbnail_factory import ThumbnailGeneratorInstance -from arches.app.models.fields.i18n import I18n_TextField, I18n_JSONField -from arches.app.models.utils import add_to_update_fields -from arches.app.utils.betterJSONSerializer import JSONSerializer -from arches.app.utils import import_class_from_string +import django.utils.timezone from django.contrib.auth.models import Group, User from django.contrib.gis.db import models -from django.core import checks -from django.core.exceptions import ObjectDoesNotExist -from django.db import ProgrammingError, connection -from django.db.models import JSONField -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import RegexValidator, validate_slug -from django.db.models import JSONField, Max, Q +from django.db import ProgrammingError, connection +from django.db.models import Case, F, JSONField, Max, Q, Value, When from django.db.models.constraints import UniqueConstraint +from django.db.models.expressions import CombinedExpression +from django.db.models.functions import Concat from django.utils import translation from django.utils.translation import gettext_lazy as _ -from django.contrib.auth.models import User -from django.contrib.auth.models import Group -from django.core.validators import validate_slug -from django.core.exceptions import ValidationError + +from arches.app.const import ExtensionType +from arches.app.models.fields.i18n import I18n_TextField, I18n_JSONField +from arches.app.models.functions import UUID4 +from arches.app.models.mixins import SaveSupportsBlindOverwriteMixin +from arches.app.models.utils import add_to_update_fields +from arches.app.utils import import_class_from_string +from arches.app.utils.betterJSONSerializer import JSONSerializer +from arches.app.utils.module_importer import get_class_from_modulename +from arches.app.utils.thumbnail_factory import ThumbnailGeneratorInstance # can't use "arches.app.models.system_settings.SystemSettings" because of circular refernce issue # so make sure the only settings we use in this file are ones that are static (fixed at run time) @@ -59,8 +46,8 @@ class Meta: db_table = "bulk_index_queue" -class CardModel(models.Model): - cardid = models.UUIDField(primary_key=True) +class CardModel(SaveSupportsBlindOverwriteMixin, models.Model): + cardid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) name = I18n_TextField(blank=True, null=True) description = I18n_TextField(blank=True, null=True) instructions = I18n_TextField(blank=True, null=True) @@ -94,8 +81,6 @@ class CardModel(models.Model): def __init__(self, *args, **kwargs): super(CardModel, self).__init__(*args, **kwargs) - if not self.cardid: - self.cardid = uuid.uuid4() if isinstance(self.cardid, str): self.cardid = uuid.UUID(self.cardid) @@ -110,41 +95,37 @@ class Meta: db_table = "cards" -class ConstraintModel(models.Model): - constraintid = models.UUIDField(primary_key=True) +class ConstraintModel(SaveSupportsBlindOverwriteMixin, models.Model): + constraintid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) uniquetoallinstances = models.BooleanField(default=False) card = models.ForeignKey("CardModel", db_column="cardid", on_delete=models.CASCADE) nodes = models.ManyToManyField(to="Node", through="ConstraintXNode") - def __init__(self, *args, **kwargs): - super(ConstraintModel, self).__init__(*args, **kwargs) - if not self.constraintid: - self.constraintid = uuid.uuid4() - class Meta: managed = True db_table = "card_constraints" -class ConstraintXNode(models.Model): - id = models.UUIDField(primary_key=True, serialize=False) +class ConstraintXNode(SaveSupportsBlindOverwriteMixin, models.Model): + id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) constraint = models.ForeignKey( "ConstraintModel", on_delete=models.CASCADE, db_column="constraintid" ) node = models.ForeignKey("Node", on_delete=models.CASCADE, db_column="nodeid") - def __init__(self, *args, **kwargs): - super(ConstraintXNode, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "constraints_x_nodes" -class CardComponent(models.Model): - componentid = models.UUIDField(primary_key=True) +class CardComponent(SaveSupportsBlindOverwriteMixin, models.Model): + componentid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) component = models.TextField() @@ -156,18 +137,13 @@ def defaultconfig_json(self): json_string = json.dumps(self.defaultconfig) return json_string - def __init__(self, *args, **kwargs): - super(CardComponent, self).__init__(*args, **kwargs) - if not self.componentid: - self.componentid = uuid.uuid4() - class Meta: managed = True db_table = "card_components" -class CardXNodeXWidget(models.Model): - id = models.UUIDField(primary_key=True) +class CardXNodeXWidget(SaveSupportsBlindOverwriteMixin, models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) node = models.ForeignKey("Node", db_column="nodeid", on_delete=models.CASCADE) card = models.ForeignKey("CardModel", db_column="cardid", on_delete=models.CASCADE) widget = models.ForeignKey("Widget", db_column="widgetid", on_delete=models.CASCADE) @@ -183,11 +159,6 @@ class CardXNodeXWidget(models.Model): on_delete=models.CASCADE, ) - def __init__(self, *args, **kwargs): - super(CardXNodeXWidget, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - def save(self, **kwargs): if self.pk == self.source_identifier_id: self.source_identifier_id = None @@ -200,18 +171,15 @@ class Meta: unique_together = (("node", "card", "widget"),) -class Concept(models.Model): - conceptid = models.UUIDField(primary_key=True) +class Concept(SaveSupportsBlindOverwriteMixin, models.Model): + conceptid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) nodetype = models.ForeignKey( "DNodeType", db_column="nodetype", on_delete=models.CASCADE ) legacyoid = models.TextField(unique=True) - def __init__(self, *args, **kwargs): - super(Concept, self).__init__(*args, **kwargs) - if not self.conceptid: - self.conceptid = uuid.uuid4() - class Meta: managed = True db_table = "concepts" @@ -273,8 +241,8 @@ class Meta: db_table = "d_value_types" -class Edge(models.Model): - edgeid = models.UUIDField(primary_key=True) +class Edge(SaveSupportsBlindOverwriteMixin, models.Model): + edgeid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) name = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) ontologyproperty = models.TextField(blank=True, null=True) @@ -307,8 +275,6 @@ class Edge(models.Model): def __init__(self, *args, **kwargs): super(Edge, self).__init__(*args, **kwargs) - if not self.edgeid: - self.edgeid = uuid.uuid4() if isinstance(self.edgeid, str): self.edgeid = uuid.UUID(self.edgeid) @@ -324,9 +290,11 @@ class Meta: unique_together = (("rangenode", "domainnode"),) -class EditLog(models.Model): - editlogid = models.UUIDField(primary_key=True) - transactionid = models.UUIDField(default=uuid.uuid1) +class EditLog(SaveSupportsBlindOverwriteMixin, models.Model): + editlogid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) + transactionid = models.UUIDField(default=uuid.uuid4) resourcedisplayname = models.TextField(blank=True, null=True) resourceclassid = models.TextField(blank=True, null=True) resourceinstanceid = models.TextField(blank=True, null=True) @@ -352,11 +320,6 @@ class EditLog(models.Model): provisional_edittype = models.TextField(blank=True, null=True) note = models.TextField(blank=True, null=True) - def __init__(self, *args, **kwargs): - super(EditLog, self).__init__(*args, **kwargs) - if not self.editlogid: - self.editlogid = uuid.uuid4() - class Meta: managed = True db_table = "edit_log" @@ -366,8 +329,10 @@ class Meta: ] -class ExternalOauthToken(models.Model): - token_id = models.UUIDField(primary_key=True, serialize=False, unique=True) +class ExternalOauthToken(SaveSupportsBlindOverwriteMixin, models.Model): + token_id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) user = models.ForeignKey( db_column="userid", null=False, @@ -381,37 +346,26 @@ class ExternalOauthToken(models.Model): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - def __init__(self, *args, **kwargs): - super(ExternalOauthToken, self).__init__(*args, **kwargs) - if not self.token_id: - self.token_id = uuid.uuid4() - class Meta: managed = True db_table = "external_oauth_tokens" -class ResourceRevisionLog(models.Model): - logid = models.UUIDField(primary_key=True) - resourceid = models.UUIDField(default=uuid.uuid1) - revisionid = models.TextField( - null=False - ) # not a ForeignKey so we can track deletions +class ResourceRevisionLog(SaveSupportsBlindOverwriteMixin, models.Model): + logid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) + resourceid = models.UUIDField(default=uuid.uuid4) + # not a ForeignKey so we can track deletions + revisionid = models.TextField(null=False) synctimestamp = models.DateTimeField(auto_now_add=True, null=False) action = models.TextField(blank=True, null=True) - def __init__(self, *args, **kwargs): - super(ResourceRevisionLog, self).__init__(*args, **kwargs) - if not self.logid: - self.logid = uuid.uuid4() - class Meta: managed = True db_table = "resource_revision_log" -class File(models.Model): - fileid = models.UUIDField(primary_key=True) +class File(SaveSupportsBlindOverwriteMixin, models.Model): + fileid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) path = models.FileField( upload_to=import_class_from_string(settings.FILENAME_GENERATOR) ) @@ -420,14 +374,9 @@ class File(models.Model): ) thumbnail_data = models.BinaryField(null=True) - def __init__(self, *args, **kwargs): - super(File, self).__init__(*args, **kwargs) - if not self.fileid: - self.fileid = uuid.uuid4() - def save(self, **kwargs): self.make_thumbnail(kwargs) - super(File, self).save(**kwargs) + super().save(**kwargs) def make_thumbnail(self, kwargs_from_save_call, force=False): try: @@ -445,24 +394,21 @@ class Meta: db_table = "files" -class TempFile(models.Model): - fileid = models.UUIDField(primary_key=True) +class TempFile(SaveSupportsBlindOverwriteMixin, models.Model): + fileid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) path = models.FileField(upload_to="archestemp") created = models.DateTimeField(auto_now_add=True) source = models.TextField() - def __init__(self, *args, **kwargs): - super(TempFile, self).__init__(*args, **kwargs) - if not self.fileid: - self.fileid = uuid.uuid4() - class Meta: managed = True db_table = "files_temporary" -class Function(models.Model): - functionid = models.UUIDField(primary_key=True) +class Function(SaveSupportsBlindOverwriteMixin, models.Model): + functionid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField(blank=True, null=True) functiontype = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) @@ -471,11 +417,6 @@ class Function(models.Model): classname = models.TextField(blank=True, null=True) component = models.TextField(blank=True, null=True) - def __init__(self, *args, **kwargs): - super(Function, self).__init__(*args, **kwargs) - if not self.functionid: - self.functionid = uuid.uuid4() - class Meta: managed = True db_table = "functions" @@ -491,8 +432,10 @@ def get_class_module(self): ) -class FunctionXGraph(models.Model): - id = models.UUIDField(primary_key=True, serialize=False) +class FunctionXGraph(SaveSupportsBlindOverwriteMixin, models.Model): + id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) function = models.ForeignKey( "Function", on_delete=models.CASCADE, db_column="functionid" ) @@ -501,19 +444,14 @@ class FunctionXGraph(models.Model): ) config = JSONField(blank=True, null=True) - def __init__(self, *args, **kwargs): - super(FunctionXGraph, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "functions_x_graphs" unique_together = ("function", "graph") -class GraphModel(models.Model): - graphid = models.UUIDField(primary_key=True) +class GraphModel(SaveSupportsBlindOverwriteMixin, models.Model): + graphid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) name = I18n_TextField(blank=True, null=True) description = I18n_TextField(blank=True, null=True) deploymentfile = models.TextField(blank=True, null=True) @@ -616,11 +554,6 @@ def save(self, **kwargs): def __str__(self): return str(self.name) - def __init__(self, *args, **kwargs): - super(GraphModel, self).__init__(*args, **kwargs) - if not self.graphid: - self.graphid = uuid.uuid4() - class Meta: managed = True db_table = "graphs" @@ -647,7 +580,7 @@ class Meta: class GraphXPublishedGraph(models.Model): publicationid = models.UUIDField( - primary_key=True, serialize=False, default=uuid.uuid1 + primary_key=True, serialize=False, default=uuid.uuid4 ) notes = models.TextField(blank=True, null=True) graph = models.ForeignKey(GraphModel, db_column="graphid", on_delete=models.CASCADE) @@ -706,8 +639,10 @@ class Meta: db_table = "languages" -class NodeGroup(models.Model): - nodegroupid = models.UUIDField(primary_key=True) +class NodeGroup(SaveSupportsBlindOverwriteMixin, models.Model): + nodegroupid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) legacygroupid = models.TextField(blank=True, null=True) cardinality = models.CharField( max_length=1, blank=True, default="1", choices={"1": "1", "n": "n"} @@ -731,11 +666,6 @@ class NodeGroup(models.Model): related_name="grouping_node_nodegroup", ) - def __init__(self, *args, **kwargs): - super(NodeGroup, self).__init__(*args, **kwargs) - if not self.nodegroupid: - self.nodegroupid = uuid.uuid4() - class Meta: managed = True db_table = "node_groups" @@ -756,20 +686,13 @@ class Meta: ) -class Node(models.Model): +class Node(SaveSupportsBlindOverwriteMixin, models.Model): """ Name is unique across all resources because it ties a node to values within tiles. Recommend prepending resource class to node name. """ - def __init__(self, *args, **kwargs): - super(Node, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - if isinstance(self.id, str): - self.id = uuid.UUID(self.id) - - nodeid = models.UUIDField(primary_key=True) + nodeid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) name = models.TextField() description = models.TextField(blank=True, null=True) istopnode = models.BooleanField() @@ -888,8 +811,8 @@ def serialize(self, fields=None, exclude=None, **kwargs): def __init__(self, *args, **kwargs): super(Node, self).__init__(*args, **kwargs) - if not self.nodeid: - self.nodeid = uuid.uuid4() + if isinstance(self.nodeid, str): + self.nodeid = uuid.UUID(self.nodeid) def clean(self): if not self.alias: @@ -925,8 +848,10 @@ class Meta: ] -class Ontology(models.Model): - ontologyid = models.UUIDField(primary_key=True) +class Ontology(SaveSupportsBlindOverwriteMixin, models.Model): + ontologyid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField() version = models.TextField() path = models.TextField(null=True, blank=True) @@ -940,17 +865,12 @@ class Ontology(models.Model): on_delete=models.CASCADE, ) - def __init__(self, *args, **kwargs): - super(Ontology, self).__init__(*args, **kwargs) - if not self.ontologyid: - self.ontologyid = uuid.uuid4() - class Meta: managed = True db_table = "ontologies" -class OntologyClass(models.Model): +class OntologyClass(SaveSupportsBlindOverwriteMixin, models.Model): """ the target JSONField has this schema: @@ -987,7 +907,9 @@ class OntologyClass(models.Model): """ - ontologyclassid = models.UUIDField(primary_key=True) + ontologyclassid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) source = models.TextField() target = JSONField(null=True) ontology = models.ForeignKey( @@ -997,11 +919,6 @@ class OntologyClass(models.Model): on_delete=models.CASCADE, ) - def __init__(self, *args, **kwargs): - super(OntologyClass, self).__init__(*args, **kwargs) - if not self.ontologyclassid: - self.ontologyclassid = uuid.uuid4() - class Meta: managed = True db_table = "ontologyclasses" @@ -1030,7 +947,7 @@ class Meta: class PublishedGraphEdit(models.Model): - edit_id = models.UUIDField(primary_key=True, serialize=False, default=uuid.uuid1) + edit_id = models.UUIDField(primary_key=True, serialize=False, default=uuid.uuid4) edit_time = models.DateTimeField(default=datetime.datetime.now, null=False) publication = models.ForeignKey( GraphXPublishedGraph, db_column="publicationid", on_delete=models.CASCADE @@ -1043,7 +960,7 @@ class Meta: db_table = "published_graph_edits" -class Relation(models.Model): +class Relation(SaveSupportsBlindOverwriteMixin, models.Model): conceptfrom = models.ForeignKey( Concept, db_column="conceptidfrom", @@ -1059,21 +976,47 @@ class Relation(models.Model): relationtype = models.ForeignKey( DRelationType, db_column="relationtype", on_delete=models.CASCADE ) - relationid = models.UUIDField(primary_key=True) - - def __init__(self, *args, **kwargs): - super(Relation, self).__init__(*args, **kwargs) - if not self.relationid: - self.relationid = uuid.uuid4() + relationid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) class Meta: managed = True db_table = "relations" - unique_together = (("conceptfrom", "conceptto", "relationtype"),) + constraints = [ + models.UniqueConstraint( + Case( + When( + CombinedExpression( + F("conceptfrom"), + "<", + F("conceptto"), + output_field=models.BooleanField(), + ), + then=Concat( + F("conceptfrom"), + Value(","), + F("conceptto"), + output_field=models.TextField(), + ), + ), + default=Concat( + F("conceptto"), + Value(","), + F("conceptfrom"), + output_field=models.TextField(), + ), + ), + "relationtype", + name="unique_relation_bidirectional", + ), + ] -class ReportTemplate(models.Model): - templateid = models.UUIDField(primary_key=True) +class ReportTemplate(SaveSupportsBlindOverwriteMixin, models.Model): + templateid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) preload_resource_data = models.BooleanField(default=True) name = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) @@ -1086,18 +1029,15 @@ def defaultconfig_json(self): json_string = json.dumps(self.defaultconfig) return json_string - def __init__(self, *args, **kwargs): - super(ReportTemplate, self).__init__(*args, **kwargs) - if not self.templateid: - self.templateid = uuid.uuid4() - class Meta: managed = True db_table = "report_templates" -class Resource2ResourceConstraint(models.Model): - resource2resourceid = models.UUIDField(primary_key=True) +class Resource2ResourceConstraint(SaveSupportsBlindOverwriteMixin, models.Model): + resource2resourceid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) resourceclassfrom = models.ForeignKey( Node, db_column="resourceclassfrom", @@ -1115,18 +1055,15 @@ class Resource2ResourceConstraint(models.Model): on_delete=models.SET_NULL, ) - def __init__(self, *args, **kwargs): - super(Resource2ResourceConstraint, self).__init__(*args, **kwargs) - if not self.resource2resourceid: - self.resource2resourceid = uuid.uuid4() - class Meta: managed = True db_table = "resource_2_resource_constraints" -class ResourceXResource(models.Model): - resourcexid = models.UUIDField(primary_key=True) +class ResourceXResource(SaveSupportsBlindOverwriteMixin, models.Model): + resourcexid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) resourceinstanceidfrom = models.ForeignKey( "ResourceInstance", db_column="resourceinstanceidfrom", @@ -1225,18 +1162,15 @@ def save(self, **kwargs): super(ResourceXResource, self).save(**kwargs) - def __init__(self, *args, **kwargs): - super(ResourceXResource, self).__init__(*args, **kwargs) - if not self.resourcexid: - self.resourcexid = uuid.uuid4() - class Meta: managed = True db_table = "resource_x_resource" -class ResourceInstance(models.Model): - resourceinstanceid = models.UUIDField(primary_key=True, blank=True) +class ResourceInstance(SaveSupportsBlindOverwriteMixin, models.Model): + resourceinstanceid = models.UUIDField( + primary_key=True, blank=True, default=uuid.uuid4, db_default=UUID4() + ) graph = models.ForeignKey( GraphModel, blank=True, db_column="graphid", on_delete=models.CASCADE ) @@ -1281,11 +1215,6 @@ class Meta: db_table = "resource_instances" permissions = (("no_access_to_resourceinstance", "No Access"),) - def __init__(self, *args, **kwargs): - super(ResourceInstance, self).__init__(*args, **kwargs) - if not self.resourceinstanceid: - self.resourceinstanceid = uuid.uuid4() - def __repr__(self): return f"<{self.graph.name}: {self.name} ({self.pk})>" @@ -1457,8 +1386,10 @@ class Meta: managed = True -class SearchComponent(models.Model): - searchcomponentid = models.UUIDField(primary_key=True) +class SearchComponent(SaveSupportsBlindOverwriteMixin, models.Model): + searchcomponentid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField() icon = models.TextField(default=None) modulename = models.TextField(blank=True, null=True) @@ -1471,11 +1402,6 @@ class SearchComponent(models.Model): def __str__(self): return self.name - def __init__(self, *args, **kwargs): - super(SearchComponent, self).__init__(*args, **kwargs) - if not self.searchcomponentid: - self.searchcomponentid = uuid.uuid4() - class Meta: managed = True db_table = "search_component" @@ -1489,8 +1415,10 @@ def toJSON(self): return JSONSerializer().serialize(self) -class SearchExportHistory(models.Model): - searchexportid = models.UUIDField(primary_key=True) +class SearchExportHistory(SaveSupportsBlindOverwriteMixin, models.Model): + searchexportid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) user = models.ForeignKey(User, on_delete=models.CASCADE) exporttime = models.DateTimeField(auto_now_add=True) numberofinstances = models.IntegerField() @@ -1499,17 +1427,12 @@ class SearchExportHistory(models.Model): upload_to="export_deliverables", blank=True, null=True ) - def __init__(self, *args, **kwargs): - super(SearchExportHistory, self).__init__(*args, **kwargs) - if not self.searchexportid: - self.searchexportid = uuid.uuid4() - class Meta: managed = True db_table = "search_export_history" -class TileModel(models.Model): # Tile +class TileModel(SaveSupportsBlindOverwriteMixin, models.Model): # Tile """ the data JSONField has this schema: @@ -1572,7 +1495,9 @@ class TileModel(models.Model): # Tile """ - tileid = models.UUIDField(primary_key=True, blank=True) + tileid = models.UUIDField( + primary_key=True, blank=True, default=uuid.uuid4, db_default=UUID4() + ) resourceinstance = models.ForeignKey( ResourceInstance, db_column="resourceinstanceid", on_delete=models.CASCADE ) @@ -1594,11 +1519,6 @@ class Meta: managed = True db_table = "tiles" - def __init__(self, *args, **kwargs): - super(TileModel, self).__init__(*args, **kwargs) - if not self.tileid: - self.tileid = uuid.uuid4() - def __repr__(self): return f"<{self.find_nodegroup_alias()} ({self.pk})>" @@ -1663,8 +1583,8 @@ def _handle_programming_error(self, error, nodegroup_alias=None): raise TileCardinalityError(message) from error -class Value(models.Model): - valueid = models.UUIDField(primary_key=True) +class Value(SaveSupportsBlindOverwriteMixin, models.Model): + valueid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) concept = models.ForeignKey( "Concept", db_column="conceptid", on_delete=models.CASCADE ) @@ -1681,18 +1601,13 @@ class Value(models.Model): on_delete=models.CASCADE, ) - def __init__(self, *args, **kwargs): - super(Value, self).__init__(*args, **kwargs) - if not self.valueid: - self.valueid = uuid.uuid4() - class Meta: managed = True db_table = "values" -class FileValue(models.Model): - valueid = models.UUIDField(primary_key=True) +class FileValue(SaveSupportsBlindOverwriteMixin, models.Model): + valueid = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) concept = models.ForeignKey( "Concept", db_column="conceptid", on_delete=models.CASCADE ) @@ -1709,11 +1624,6 @@ class FileValue(models.Model): on_delete=models.CASCADE, ) - def __init__(self, *args, **kwargs): - super(FileValue, self).__init__(*args, **kwargs) - if not self.valueid: - self.valueid = uuid.uuid4() - class Meta: managed = False db_table = "values" @@ -1729,8 +1639,10 @@ def getname(self): return "" -class Widget(models.Model): - widgetid = models.UUIDField(primary_key=True) +class Widget(SaveSupportsBlindOverwriteMixin, models.Model): + widgetid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField(unique=True) component = models.TextField(unique=True) defaultconfig = JSONField(blank=True, null=True, db_column="defaultconfig") @@ -1745,18 +1657,15 @@ def defaultconfig_json(self): def __str__(self): return self.name - def __init__(self, *args, **kwargs): - super(Widget, self).__init__(*args, **kwargs) - if not self.widgetid: - self.widgetid = uuid.uuid4() - class Meta: managed = True db_table = "widgets" -class Geocoder(models.Model): - geocoderid = models.UUIDField(primary_key=True) +class Geocoder(SaveSupportsBlindOverwriteMixin, models.Model): + geocoderid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField(unique=True) component = models.TextField(unique=True) api_key = models.TextField(blank=True, null=True) @@ -1764,11 +1673,6 @@ class Geocoder(models.Model): def __str__(self): return self.name - def __init__(self, *args, **kwargs): - super(Geocoder, self).__init__(*args, **kwargs) - if not self.geocoderid: - self.geocoderid = uuid.uuid4() - class Meta: managed = True db_table = "geocoders" @@ -1791,8 +1695,10 @@ class Meta: db_table = "map_sources" -class MapLayer(models.Model): - maplayerid = models.UUIDField(primary_key=True) +class MapLayer(SaveSupportsBlindOverwriteMixin, models.Model): + maplayerid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField(unique=True) layerdefinitions = JSONField(blank=True, null=True, db_column="layerdefinitions") isoverlay = models.BooleanField(default=False) @@ -1815,11 +1721,6 @@ def layer_json(self): def __str__(self): return self.name - def __init__(self, *args, **kwargs): - super(MapLayer, self).__init__(*args, **kwargs) - if not self.maplayerid: - self.maplayerid = uuid.uuid4() - class Meta: managed = True ordering = ("sortorder", "name") @@ -1833,18 +1734,15 @@ class Meta: ) -class GraphXMapping(models.Model): - id = models.UUIDField(primary_key=True, serialize=False) +class GraphXMapping(SaveSupportsBlindOverwriteMixin, models.Model): + id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) graph = models.ForeignKey( "GraphModel", db_column="graphid", on_delete=models.CASCADE ) mapping = JSONField(blank=True, null=False) - def __init__(self, *args, **kwargs): - super(GraphXMapping, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "graphs_x_mapping_file" @@ -1855,14 +1753,6 @@ class UserProfile(models.Model): phone = models.CharField(max_length=16, blank=True) encrypted_mfa_hash = models.CharField(max_length=128, null=True, blank=True) - def is_reviewer(self): - """DEPRECATED Use new pattern: - - from arches.app.utils.permission_backend import user_is_resource_reviewer - is_reviewer = user_is_resource_reviewer(user) - """ - pass - @property def viewable_nodegroups(self): from arches.app.utils.permission_backend import get_nodegroups_by_perm @@ -1901,8 +1791,10 @@ class Meta: db_table = "user_profile" -class UserXTask(models.Model): - id = models.UUIDField(primary_key=True, serialize=False) +class UserXTask(SaveSupportsBlindOverwriteMixin, models.Model): + id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) taskid = models.UUIDField(serialize=False, blank=True, null=True) status = models.TextField(null=True, default="PENDING") datestart = models.DateTimeField(blank=True, null=True) @@ -1910,45 +1802,39 @@ class UserXTask(models.Model): name = models.TextField(blank=True, null=True) user = models.ForeignKey(User, on_delete=models.CASCADE) - def __init__(self, *args, **kwargs): - super(UserXTask, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "user_x_tasks" -class NotificationType(models.Model): +class NotificationType(SaveSupportsBlindOverwriteMixin, models.Model): """ Creates a 'type' of notification that would be associated with a specific trigger, e.g. Search Export Complete or Package Load Complete Must be created manually using Django ORM or SQL. """ - typeid = models.UUIDField(primary_key=True, serialize=False) + typeid = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) name = models.TextField(blank=True, null=True) emailtemplate = models.TextField(blank=True, null=True) emailnotify = models.BooleanField(default=False) webnotify = models.BooleanField(default=False) - def __init__(self, *args, **kwargs): - super(NotificationType, self).__init__(*args, **kwargs) - if not self.typeid: - self.typeid = uuid.uuid4() - class Meta: managed = True db_table = "notification_types" -class Notification(models.Model): +class Notification(SaveSupportsBlindOverwriteMixin, models.Model): """ A Notification instance that may optionally have a NotificationType. Can spawn N UserXNotification instances Must be created manually using Django ORM. """ - id = models.UUIDField(primary_key=True, serialize=False) + id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) created = models.DateTimeField(auto_now_add=True) # created.editable = True message = models.TextField(blank=True, null=True) @@ -1956,17 +1842,12 @@ class Notification(models.Model): # TODO: Ideally validate context against a list of keys from NotificationType notiftype = models.ForeignKey(NotificationType, on_delete=models.CASCADE, null=True) - def __init__(self, *args, **kwargs): - super(Notification, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "notifications" -class UserXNotification(models.Model): +class UserXNotification(SaveSupportsBlindOverwriteMixin, models.Model): """ A UserXNotification instance depends on an existing Notification instance and a User. If its Notification instance has a NotificationType, this Type can be overriden for this particular User with a UserXNotificationType. @@ -1975,22 +1856,19 @@ class UserXNotification(models.Model): Property 'isread' refers to either webnotify or emailnotify, not both, behaves differently. """ - id = models.UUIDField(primary_key=True, serialize=False) + id = models.UUIDField( + primary_key=True, serialize=False, default=uuid.uuid4, db_default=UUID4() + ) notif = models.ForeignKey(Notification, on_delete=models.CASCADE) isread = models.BooleanField(default=False) recipient = models.ForeignKey(User, on_delete=models.CASCADE) - def __init__(self, *args, **kwargs): - super(UserXNotification, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "user_x_notifications" -class UserXNotificationType(models.Model): +class UserXNotificationType(SaveSupportsBlindOverwriteMixin, models.Model): """ A UserXNotificationType instance only exists as an override of an existing NotificationType and is user-specific and notification-settings-specific (e.g. emailnotify, webnotify, etc.) @@ -1999,17 +1877,12 @@ class UserXNotificationType(models.Model): UserXNotificationTypes are automatically queried and applied as filters in get() requests for UserXNotifications in views/notifications """ - id = models.UUIDField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, db_default=UUID4()) user = models.ForeignKey(User, on_delete=models.CASCADE) notiftype = models.ForeignKey(NotificationType, on_delete=models.CASCADE) emailnotify = models.BooleanField(default=False) webnotify = models.BooleanField(default=False) - def __init__(self, *args, **kwargs): - super(UserXNotificationType, self).__init__(*args, **kwargs) - if not self.id: - self.id = uuid.uuid4() - class Meta: managed = True db_table = "user_x_notification_types" @@ -2031,8 +1904,10 @@ class Meta: db_table = "map_markers" -class Plugin(models.Model): - pluginid = models.UUIDField(primary_key=True) +class Plugin(SaveSupportsBlindOverwriteMixin, models.Model): + pluginid = models.UUIDField( + primary_key=True, default=uuid.uuid4, db_default=UUID4() + ) name = I18n_TextField(null=True, blank=True) icon = models.TextField(default=None) component = models.TextField() @@ -2042,11 +1917,6 @@ class Plugin(models.Model): sortorder = models.IntegerField(blank=True, null=True, default=None) helptemplate = models.TextField(blank=True, null=True) - def __init__(self, *args, **kwargs): - super(Plugin, self).__init__(*args, **kwargs) - if not self.pluginid: - self.pluginid = uuid.uuid4() - def __str__(self): return str(self.name) @@ -2153,11 +2023,6 @@ class VwAnnotation(models.Model): feature = JSONField() canvas = models.TextField() - def __init__(self, *args, **kwargs): - super(VwAnnotation, self).__init__(*args, **kwargs) - if not self.feature_id: - self.feature_id = uuid.uuid4() - class Meta: managed = False db_table = "vw_annotations" @@ -2178,7 +2043,7 @@ class Meta: class ETLModule(models.Model): - etlmoduleid = models.UUIDField(primary_key=True, default=uuid.uuid1) + etlmoduleid = models.UUIDField(primary_key=True, default=uuid.uuid4) name = models.TextField() icon = models.TextField() etl_type = models.TextField() @@ -2277,7 +2142,7 @@ class Meta: class SpatialView(models.Model): - spatialviewid = models.UUIDField(primary_key=True, default=uuid.uuid1) + spatialviewid = models.UUIDField(primary_key=True, default=uuid.uuid4) schema = models.TextField(default="public") slug = models.TextField( validators=[ diff --git a/arches/db/migration_operations/extras.py b/arches/db/migration_operations/extras.py index f160a10c28b..5f1056f06eb 100644 --- a/arches/db/migration_operations/extras.py +++ b/arches/db/migration_operations/extras.py @@ -106,34 +106,3 @@ def database_backwards(self, app_label, schema_editor, from_state, to_state): def describe(self): return "Creates a function named %s" % self.name return "Creates extension %s" % self.name - - -class CreateAutoPopulateUUIDField(Operation): - def __init__(self, table, columns): - self.table = table - self.columns = columns - - def state_forwards(self, app_label, state): - pass - - def database_forwards(self, app_label, schema_editor, from_state, to_state): - for column in self.columns: - schema_editor.execute( - "ALTER TABLE {0} ALTER COLUMN {1} SET DEFAULT uuid_generate_v1mc()".format( - self.table, column - ) - ) - - def database_backwards(self, app_label, schema_editor, from_state, to_state): - for column in self.columns: - schema_editor.execute( - "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT".format( - self.table, column - ) - ) - - def describe(self): - return ( - "Sets default value for uuid column(s) %s in %s" % self.columns, - self.table, - ) diff --git a/releases/8.0.0.md b/releases/8.0.0.md index 39cc72732e6..d43d447d0b5 100644 --- a/releases/8.0.0.md +++ b/releases/8.0.0.md @@ -29,6 +29,7 @@ Arches 8.0.0 Release Notes - Add alt text on image placeholder [#10455](https://github.com/archesproject/arches/issues/10455) - Make failed login alert message dismissible [#11767](https://github.com/archesproject/arches/issues/11767) - Concepts API no longer responds with empty body for error conditions [#11519](https://github.com/archesproject/arches/issues/11519) +- Restore support for `.create()` on Django model managers [#10958](https://github.com/archesproject/arches/issues/10958) - Removes sample index from new projects, updates test coverage behavior [#11591](https://github.com/archesproject/arches/issues/11519) - Fix HTTP return code for tile validation and cardinality errors [#11803](https://github.com/archesproject/arches/issues/11803) - Add system dark mode detection for Vue apps [#11624](https://github.com/archesproject/arches/issues/11624) diff --git a/tests/models/relation_tests.py b/tests/models/relation_tests.py new file mode 100644 index 00000000000..da36107aaf5 --- /dev/null +++ b/tests/models/relation_tests.py @@ -0,0 +1,16 @@ +from django.db import IntegrityError + +from arches.app.models.models import Relation +from tests.base_test import ArchesTestCase + + +class RelationTests(ArchesTestCase): + def test_bidirectional_duplicate_check(self): + relation = Relation.objects.first() + duplicate = Relation( + conceptfrom_id=relation.conceptto_id, + conceptto_id=relation.conceptfrom_id, + relationtype=relation.relationtype, + ) + with self.assertRaises(IntegrityError): + duplicate.save()