Skip to content

Commit

Permalink
Merge branch 'production' into issue-5331
Browse files Browse the repository at this point in the history
  • Loading branch information
grantfitzsimmons authored Oct 22, 2024
2 parents bf6fdcb + dfe5887 commit 781b6fd
Show file tree
Hide file tree
Showing 28 changed files with 1,005 additions and 226 deletions.
14 changes: 7 additions & 7 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ DATABASE_PORT=3306
MYSQL_ROOT_PASSWORD=password
DATABASE_NAME=specify

# When running Specify 7 for the first time or during updates that
# require migrations, ensure that the MASTER_NAME and MASTER_PASSWORD
# are set to the root username and password. This will ensure proper
# execution of Django migrations during the ixnitial setup.
# After launching Specify and verifying the update is complete, you can
# When running Specify 7 for the first time or during updates that
# require migrations, ensure that the MASTER_NAME and MASTER_PASSWORD
# are set to the root username and password. This will ensure proper
# execution of Django migrations during the initial setup.
# After launching Specify and verifying the update is complete, you can
# safely replace these credentials with the master SQL user name and password.
MASTER_NAME=root
MASTER_PASSWORD=password
Expand Down Expand Up @@ -36,7 +36,7 @@ CELERY_RESULT_BACKEND=redis://redis/1
LOG_LEVEL=WARNING

# Set this variable to `true` to run Specify 7 in debug mode. This
# should only be used during development and troubleshooting and not
# during general use. Django applications leak memory when operated
# should only be used during development and troubleshooting and not
# during general use. Django applications leak memory when operated
# continuously in debug mode.
SP7_DEBUG=true
50 changes: 45 additions & 5 deletions specifyweb/businessrules/migrations/0002_default_unique_rules.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,53 @@
from django.db import migrations

from specifyweb.specify import models as spmodels
from specifyweb.businessrules.uniqueness_rules import apply_default_uniqueness_rules
from specifyweb.specify.datamodel import datamodel
from specifyweb.businessrules.uniqueness_rules import apply_default_uniqueness_rules, rule_is_global, DEFAULT_UNIQUENESS_RULES


def apply_rules_to_discipline(apps, schema_editor):
for disp in spmodels.Discipline.objects.all():
def apply_default_rules(apps, schema_editor):
Discipline = apps.get_model('specify', 'Discipline')
for disp in Discipline.objects.all():
apply_default_uniqueness_rules(disp)


def remove_default_rules(apps, schema_editor):
Discipline = apps.get_model('specify', 'Discipline')
UniquenessRule = apps.get_model('businessrules', 'UniquenessRule')
UniquenessRuleFields = apps.get_model(
'businessrules', 'UniquenessRuleField')

for discipline in Discipline.objects.all():
remove_rules_from_discipline(
discipline, UniquenessRule, UniquenessRuleFields)


def remove_rules_from_discipline(discipline, uniqueness_rule, uniquenessrule_fields):
for table, rules in DEFAULT_UNIQUENESS_RULES.items():
model_name = datamodel.get_table_strict(table).django_name
for rule in rules:
to_remove = set()
fields, scopes = rule["rule"]
isDatabaseConstraint = rule["isDatabaseConstraint"]

is_global = rule_is_global(scopes)

for field in fields:
found_fields = uniquenessrule_fields.objects.filter(uniquenessrule__modelName=model_name, uniquenessrule__isDatabaseConstraint=isDatabaseConstraint,
uniquenessrule__discipline_id=None if is_global else discipline.id, fieldPath=field, isScope=False)

to_remove.update(
tuple(found_fields.values_list('uniquenessrule_id', flat=True)))
found_fields.delete()
for scope in scopes:
found_scopes = uniquenessrule_fields.objects.filter(uniquenessrule__modelName=model_name, uniquenessrule__isDatabaseConstraint=isDatabaseConstraint,
uniquenessrule__discipline_id=None if is_global else discipline.id, fieldPath=scope, isScope=True)

to_remove.update(
tuple(found_scopes.values_list('uniquenessrule_id', flat=True)))
found_scopes.delete()
uniqueness_rule.objects.filter(id__in=tuple(to_remove)).delete()


class Migration(migrations.Migration):
initial = True

Expand All @@ -18,5 +57,6 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(apply_rules_to_discipline),
migrations.RunPython(apply_default_rules,
remove_default_rules, atomic=True),
]
64 changes: 50 additions & 14 deletions specifyweb/businessrules/uniqueness_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
from django.db import connections
from django.db.migrations.recorder import MigrationRecorder
from django.core.exceptions import ObjectDoesNotExist
from specifyweb.specify import models
from specifyweb.specify.api import get_model
from specifyweb.specify.datamodel import datamodel
from specifyweb.middleware.general import serialize_django_obj
from specifyweb.specify.scoping import in_same_scope
from .orm_signal_handler import orm_signal_handler
from .exceptions import BusinessRuleException
from .models import UniquenessRule
from . import models

DEFAULT_UNIQUENESS_RULES: Dict[str, List[Dict[str, Union[List[List[str]], bool]]]] = json.load(
open('specifyweb/businessrules/uniqueness_rules.json'))
Expand All @@ -29,7 +29,13 @@
@orm_signal_handler('pre_save', None, dispatch_uid=UNIQUENESS_DISPATCH_UID)
def check_unique(model, instance):
model_name = instance.__class__.__name__
rules = UniquenessRule.objects.filter(modelName=model_name)
cannonical_model = get_model(model_name)

if not cannonical_model:
# The model is not a Specify Model
# probably a Django-specific model
return

applied_migrations = MigrationRecorder(
connections['default']).applied_migrations()

Expand All @@ -40,14 +46,23 @@ def check_unique(model, instance):
else:
return

# We can't directly use the main app registry in the context of migrations, which uses fake models
registry = model._meta.apps

UniquenessRule = registry.get_model('businessrules', 'UniquenessRule')
UniquenessRuleField = registry.get_model(
'businessrules', 'UniquenessRuleField')

rules = UniquenessRule.objects.filter(modelName=model_name)
for rule in rules:
if not rule_is_global(tuple(field.fieldPath for field in rule.fields.filter(isScope=True))) and not in_same_scope(rule, instance):
rule_fields = UniquenessRuleField.objects.filter(uniquenessrule=rule)
if not rule_is_global(tuple(field.fieldPath for field in rule_fields.filter(isScope=True))) and not in_same_scope(rule, instance):
continue

field_names = [
field.fieldPath.lower() for field in rule.fields.filter(isScope=False)]
field.fieldPath.lower() for field in rule_fields.filter(isScope=False)]

_scope = rule.fields.filter(isScope=True)
_scope = rule_fields.filter(isScope=True)
scope = None if len(_scope) == 0 else _scope[0]

all_fields = [*field_names]
Expand Down Expand Up @@ -138,7 +153,9 @@ def join_with_and(fields):
return ' and '.join(fields)


def apply_default_uniqueness_rules(discipline: models.Discipline):
def apply_default_uniqueness_rules(discipline, registry=None):
UniquenessRule = registry.get_model(
'businessrules', 'UniquenessRule') if registry else models.UniquenessRule
has_set_global_rules = len(
UniquenessRule.objects.filter(discipline=None)) > 0

Expand All @@ -156,15 +173,34 @@ def apply_default_uniqueness_rules(discipline: models.Discipline):
_discipline = None

create_uniqueness_rule(
model_name, _discipline, isDatabaseConstraint, fields, scopes)
model_name, _discipline, isDatabaseConstraint, fields, scopes, registry)


def create_uniqueness_rule(model_name, discipline, is_database_constraint, fields, scopes, registry=None):
UniquenessRule = registry.get_model(
'businessrules', 'UniquenessRule') if registry else models.UniquenessRule
UniquenessRuleField = registry.get_model(
'businessrules', 'UniquenessRuleField') if registry else models.UniquenessRuleField

matching_fields = UniquenessRuleField.objects.filter(
fieldPath__in=fields, uniquenessrule__modelName=model_name, uniquenessrule__isDatabaseConstraint=is_database_constraint, uniquenessrule__discipline=discipline, isScope=False)

matching_scopes = UniquenessRuleField.objects.filter(
fieldPath__in=scopes, uniquenessrule__modelName=model_name, uniquenessrule__isDatabaseConstraint=is_database_constraint, uniquenessrule__discipline=discipline, isScope=True)

# If the rule already exists, skip creating the rule
if len(matching_fields) == len(fields) and len(matching_scopes) == len(scopes):
return

rule = UniquenessRule.objects.create(
discipline=discipline, modelName=model_name, isDatabaseConstraint=is_database_constraint)

def create_uniqueness_rule(model_name, discipline, is_database_constraint, fields, scopes) -> UniquenessRule:
created_rule = UniquenessRule.objects.create(discipline=discipline,
modelName=model_name, isDatabaseConstraint=is_database_constraint)
created_rule.fields.set(fields)
created_rule.fields.add(
*scopes, through_defaults={"isScope": True})
for field in fields:
UniquenessRuleField.objects.create(
uniquenessrule=rule, fieldPath=field, isScope=False)
for scope in scopes:
UniquenessRuleField.objects.create(
uniquenessrule=rule, fieldPath=scope, isScope=True)


"""If a uniqueness rule has a scope which traverses through a hiearchy
Expand Down
Loading

0 comments on commit 781b6fd

Please sign in to comment.