diff --git a/backend/src/accounts/migrations/0146_auto_20260814_2002.py b/backend/src/accounts/migrations/0146_auto_20260814_2002.py new file mode 100644 index 000000000..274f9be41 --- /dev/null +++ b/backend/src/accounts/migrations/0146_auto_20260814_2002.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2 on 2026-08-14 20:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0145_apikey_secure_storage'), + ] + + operations = [ + migrations.AlterModelOptions( + name='apikey', + options={'ordering': ['-date_created']}, + ), + migrations.AlterField( + model_name='account', + name='max_users', + field=models.IntegerField(default=1000), + ), + ] diff --git a/backend/src/processes/enums.py b/backend/src/processes/enums.py index 47923dc58..6120b5a97 100644 --- a/backend/src/processes/enums.py +++ b/backend/src/processes/enums.py @@ -777,10 +777,63 @@ class FieldSetLayout: class FieldSetRuleType: + # TODO Deprecated + + VALIDATOR = 'validator' + SUM_EQUAL = 'sum_equal' + + CHOICES = ( + (SUM_EQUAL, 'Sum equal'), + (VALIDATOR, 'Validator'), + ) + + LITERALS = Literal[SUM_EQUAL, VALIDATOR] + + +class FieldRuleType: + + SHOW = 'show' + VALIDATOR = 'validator' + + CHOICES = ( + (SHOW, 'Show field'), + (VALIDATOR, 'Validate field'), + ) + + LITERALS = Literal[SHOW, VALIDATOR] + + +class FieldRuleOperator: + + EQUAL = 'equal' + GREATER_THAN = 'greater_than' + LESS_THAN = 'less_than' + + CHOICES = ( + (EQUAL, 'Equal'), + (GREATER_THAN, 'Greater than'), + (LESS_THAN, 'Less than'), + ) + + LITERALS = Literal[EQUAL, GREATER_THAN, LESS_THAN] + + +class FieldSetRuleOperator: + SUM_EQUAL = 'sum_equal' + SUM_GREATER_THAN = 'sum_greater_than' + SUM_LESS_THAN = 'sum_less_than' + + SUM_OPERATORS = ( + SUM_EQUAL, + SUM_GREATER_THAN, + SUM_LESS_THAN, + ) CHOICES = ( - (SUM_EQUAL, 'The sum is equal'), + (SUM_EQUAL, 'Sum equal'), + (SUM_GREATER_THAN, 'Sum greater than'), + (SUM_LESS_THAN, 'Sum less than'), ) - LITERALS = Literal[SUM_EQUAL] + LITERALS = Literal[SUM_EQUAL, SUM_GREATER_THAN, SUM_LESS_THAN] diff --git a/backend/src/processes/messages/template.py b/backend/src/processes/messages/template.py index bf1fabc39..2b1026ba9 100644 --- a/backend/src/processes/messages/template.py +++ b/backend/src/processes/messages/template.py @@ -312,3 +312,23 @@ name=name, step_name=step_name, ) + +MSG_PT_0075 = lambda task_name, field_name, api_name: format_lazy( + _( + 'Task "{task_name}": The output field "{field_name}" ' + 'rule contains a duplicate api_name "{api_name}".' + 'Recreate the rule or change its api_name.', + ), + task_name=task_name, + field_name=field_name, + api_name=api_name, +) +MSG_PT_0076 = lambda task_name, field_name, api_name: format_lazy( + _( + 'Task "{task_name}": The output field "{field_name}" ' + 'rule points to a non-existent field "{api_name}". ', + ), + task_name=task_name, + field_name=field_name, + api_name=api_name, +) diff --git a/backend/src/processes/migrations/0260_auto_20260814_2003.py b/backend/src/processes/migrations/0260_auto_20260814_2003.py new file mode 100644 index 000000000..594933c2e --- /dev/null +++ b/backend/src/processes/migrations/0260_auto_20260814_2003.py @@ -0,0 +1,475 @@ +# Generated by Django 2.2 on 2026-08-14 20:03 +from django.db import migrations, models +import django.db.models.deletion +import src.generics.mixins.models + + +def migrate_fieldset_template_rules(apps, schema_editor): + OldRule = apps.get_model('processes', 'FieldsetTemplateRule') + RuleSet = apps.get_model('processes', 'FieldSetTemplateRuleSet') + GroupOr = apps.get_model('processes', 'FieldSetTemplateRuleGroupOr') + GroupAnd = apps.get_model('processes', 'FieldSetTemplateRuleGroupAnd') + + order_by_fieldset = {} + rules = ( + OldRule.objects + .select_related('fieldset') + .prefetch_related('fields') + .order_by('fieldset_id', 'id') + ) + for rule in rules: + if RuleSet.objects.filter( + fieldset_id=rule.fieldset_id, + api_name=rule.api_name, + ).exists(): + continue + + order_in_fieldset = order_by_fieldset.get(rule.fieldset_id, 0) + order_by_fieldset[rule.fieldset_id] = order_in_fieldset + 1 + template_id = rule.fieldset.template_id + + ruleset = RuleSet.objects.create( + api_name=rule.api_name, + message=None, + order=order_in_fieldset, + fieldset_id=rule.fieldset_id, + template_id=template_id, + account_id=rule.account_id, + is_deleted=rule.is_deleted, + ) + group_or = GroupOr.objects.create( + api_name=f'{rule.api_name}-group-or', + fieldset_rule=ruleset, + template_id=template_id, + account_id=rule.account_id, + is_deleted=rule.is_deleted, + ) + GroupAnd.objects.create( + api_name=f'{rule.api_name}-group-and', + group_or=group_or, + template_id=template_id, + account_id=rule.account_id, + operator=rule.type, + value=rule.value, + is_deleted=rule.is_deleted, + ) + ruleset.fields.set(rule.fields.all()) + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0146_auto_20260814_2002'), + ('processes', '0259_populate_fieldset_title_from_name'), + ] + + operations = [ + migrations.CreateModel( + name='FieldRuleGroupAnd', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('operator', models.CharField(choices=[('equal', 'Equal'), ('greater_than', 'Greater than'), ('less_than', 'Less than')], max_length=50)), + ('value', models.CharField(blank=True, max_length=200, null=True)), + ('field', models.CharField(max_length=200, null=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldRuleGroupOr', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldRuleSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('type', models.CharField(choices=[('show', 'Show field'), ('validator', 'Validate field')], max_length=50)), + ('message', models.TextField(blank=True, help_text='custom error message for a type="validator"', null=True)), + ('order', models.PositiveIntegerField(default=0)), + ], + options={ + 'ordering': ['order', 'id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldSetRuleGroupAnd', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('operator', models.CharField(choices=[('sum_equal', 'Sum equal'), ('sum_greater_than', 'Sum greater than'), ('sum_less_than', 'Sum less than')], max_length=50)), + ('value', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldSetRuleGroupOr', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldSetRuleSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('message', models.TextField(blank=True, help_text='custom error message for a type="validator"', null=True)), + ('order', models.PositiveIntegerField(default=0)), + ], + options={ + 'ordering': ['order', 'id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldSetTemplateRuleGroupAnd', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('operator', models.CharField(choices=[('sum_equal', 'Sum equal'), ('sum_greater_than', 'Sum greater than'), ('sum_less_than', 'Sum less than')], max_length=50)), + ('value', models.CharField(blank=True, max_length=200, null=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldSetTemplateRuleGroupOr', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldSetTemplateRuleSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('message', models.TextField(blank=True, help_text='custom error message for a type="validator"', null=True)), + ('order', models.PositiveIntegerField(default=0)), + ], + options={ + 'ordering': ['order', 'id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldTemplateRuleGroupAnd', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('operator', models.CharField(choices=[('equal', 'Equal'), ('greater_than', 'Greater than'), ('less_than', 'Less than')], max_length=50)), + ('value', models.CharField(blank=True, max_length=200, null=True)), + ('field', models.CharField(max_length=200, null=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldTemplateRuleGroupOr', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ], + options={ + 'ordering': ['id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.CreateModel( + name='FieldTemplateRuleSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_deleted', models.BooleanField(default=False)), + ('api_name', models.CharField(max_length=200)), + ('type', models.CharField(choices=[('show', 'Show field'), ('validator', 'Validate field')], max_length=50)), + ('message', models.TextField(blank=True, help_text='custom error message for a type="validator"', null=True)), + ('order', models.PositiveIntegerField(default=0)), + ], + options={ + 'ordering': ['order', 'id'], + }, + bases=(src.generics.mixins.models.SoftDeleteMixin, models.Model), + ), + migrations.RemoveConstraint( + model_name='fieldsettemplate', + name='fieldsettemplate_template_api_name_is_shared_unique', + ), + migrations.RemoveConstraint( + model_name='fieldtemplate', + name='processes_fieldtemplate_template_api_name_unique', + ), + migrations.AlterField( + model_name='fieldsettemplaterule', + name='type', + field=models.CharField(choices=[('validator', 'Validator')], max_length=50), + ), + migrations.AlterField( + model_name='workflowevent', + name='type', + field=models.IntegerField(choices=[(0, 'Workflow started'), (1, 'Workflow completed'), (2, 'Task started'), (3, 'Task completed'), (4, 'Task reverted'), (9, 'Task skipped'), (5, 'New comment'), (6, 'Workflow ended'), (7, 'Workflow snoozed from template'), (17, 'Workflow snoozed'), (8, 'Workflow reverted'), (10, 'Workflow ended by condition'), (11, 'Workflow is urgent'), (12, 'Workflow is not urgent'), (13, 'Task skipped as no performers were assigned'), (14, 'Performer added to task'), (15, 'Performer deleted from task'), (16, 'Workflow resumed'), (18, 'Due date changed'), (20, 'Performer group added to task'), (21, 'Performer group deleted from task'), (22, 'Task snoozed from template'), (23, 'Task delegated')], verbose_name='type'), + ), + migrations.AddConstraint( + model_name='fieldsettemplate', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('api_name', 'template', 'is_shared', 'account'), name='fieldsettemplate_template_api_name_is_shared_unique'), + ), + migrations.AddConstraint( + model_name='fieldtemplate', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('template', 'api_name', 'account'), name='processes_fieldtemplate_template_api_name_unique'), + ), + migrations.AddField( + model_name='fieldtemplateruleset', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldtemplateruleset', + name='field', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rulesets', to='processes.FieldTemplate'), + ), + migrations.AddField( + model_name='fieldtemplateruleset', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='field_rulesets', to='processes.Template'), + ), + migrations.AddField( + model_name='fieldtemplaterulegroupor', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldtemplaterulegroupor', + name='field_rule', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_or', to='processes.FieldTemplateRuleSet'), + ), + migrations.AddField( + model_name='fieldtemplaterulegroupor', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='field_ruleset_groups_or', to='processes.Template'), + ), + migrations.AddField( + model_name='fieldtemplaterulegroupand', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldtemplaterulegroupand', + name='group_or', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_and', to='processes.FieldTemplateRuleGroupOr'), + ), + migrations.AddField( + model_name='fieldtemplaterulegroupand', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='field_rules_group_and', to='processes.Template'), + ), + migrations.AddField( + model_name='fieldsettemplateruleset', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldsettemplateruleset', + name='fields', + field=models.ManyToManyField(blank=True, related_name='fieldset_rulesets', to='processes.FieldTemplate'), + ), + migrations.AddField( + model_name='fieldsettemplateruleset', + name='fieldset', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rulesets', to='processes.FieldsetTemplate'), + ), + migrations.AddField( + model_name='fieldsettemplateruleset', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fieldset_rulesets', to='processes.Template'), + ), + migrations.AddField( + model_name='fieldsettemplaterulegroupor', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldsettemplaterulegroupor', + name='fieldset_rule', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_or', to='processes.FieldSetTemplateRuleSet'), + ), + migrations.AddField( + model_name='fieldsettemplaterulegroupor', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fieldset_ruleset_groups_or', to='processes.Template'), + ), + migrations.AddField( + model_name='fieldsettemplaterulegroupand', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldsettemplaterulegroupand', + name='group_or', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_and', to='processes.FieldSetTemplateRuleGroupOr'), + ), + migrations.AddField( + model_name='fieldsettemplaterulegroupand', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fieldset_rules_group_and', to='processes.Template'), + ), + migrations.AddField( + model_name='fieldsetruleset', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldsetruleset', + name='fields', + field=models.ManyToManyField(blank=True, related_name='fieldset_rulesets', to='processes.TaskField'), + ), + migrations.AddField( + model_name='fieldsetruleset', + name='fieldset', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rulesets', to='processes.FieldSet'), + ), + migrations.AddField( + model_name='fieldsetruleset', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fieldset_rulesets', to='processes.Workflow'), + ), + migrations.AddField( + model_name='fieldsetrulegroupor', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldsetrulegroupor', + name='fieldset_rule', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_or', to='processes.FieldSetRuleSet'), + ), + migrations.AddField( + model_name='fieldsetrulegroupor', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fieldset_ruleset_groups_or', to='processes.Workflow'), + ), + migrations.AddField( + model_name='fieldsetrulegroupand', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldsetrulegroupand', + name='group_or', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_and', to='processes.FieldSetRuleGroupOr'), + ), + migrations.AddField( + model_name='fieldsetrulegroupand', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fieldset_rules_group_and', to='processes.Workflow'), + ), + migrations.AddField( + model_name='fieldruleset', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldruleset', + name='field', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rulesets', to='processes.TaskField'), + ), + migrations.AddField( + model_name='fieldruleset', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_rulesets', to='processes.Workflow'), + ), + migrations.AddField( + model_name='fieldrulegroupor', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldrulegroupor', + name='field_rule', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_or', to='processes.FieldRuleSet'), + ), + migrations.AddField( + model_name='fieldrulegroupor', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_ruleset_groups_or', to='processes.Workflow'), + ), + migrations.AddField( + model_name='fieldrulegroupand', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Account'), + ), + migrations.AddField( + model_name='fieldrulegroupand', + name='group_or', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups_and', to='processes.FieldRuleGroupOr'), + ), + migrations.AddField( + model_name='fieldrulegroupand', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='field_rules_group_and', to='processes.Workflow'), + ), + migrations.AddConstraint( + model_name='fieldtemplateruleset', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('template', 'api_name', 'account'), name='fieldtemplateruleset_field_api_name_unique'), + ), + migrations.AddConstraint( + model_name='fieldtemplaterulegroupor', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('api_name', 'template', 'account'), name='rulegroupor_field_rule_api_name_unique'), + ), + migrations.AddConstraint( + model_name='fieldtemplaterulegroupand', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('template', 'api_name', 'account'), name='rulegroupand_group_or_api_name_unique'), + ), + migrations.AddConstraint( + model_name='fieldsettemplateruleset', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('template', 'api_name', 'account'), name='fieldsetruleset_fieldset_api_name_unique'), + ), + migrations.AddConstraint( + model_name='fieldsettemplaterulegroupor', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('api_name', 'template', 'account'), name='fieldset_rulegroupor_field_rule_api_name_unique'), + ), + migrations.AddConstraint( + model_name='fieldsettemplaterulegroupand', + constraint=models.UniqueConstraint(condition=models.Q(is_deleted=False), fields=('template', 'api_name', 'account'), name='fieldset_rulegroupand_group_or_api_name_unique'), + ), + migrations.RunPython( + migrate_fieldset_template_rules, + migrations.RunPython.noop, + ), + ] diff --git a/backend/src/processes/models/templates/fields.py b/backend/src/processes/models/templates/fields.py index 3c36b8f03..eefd5a888 100644 --- a/backend/src/processes/models/templates/fields.py +++ b/backend/src/processes/models/templates/fields.py @@ -3,8 +3,11 @@ from src.accounts.models import AccountBaseMixin from src.generics.managers import BaseSoftDeleteManager +from src.processes.enums import FieldRuleType, FieldRuleOperator from src.processes.models.base import BaseApiNameModel -from src.processes.models.mixins import FieldMixin +from src.processes.models.mixins import ( + FieldMixin, +) from src.processes.models.templates.kickoff import Kickoff from src.processes.models.templates.task import TaskTemplate from src.processes.models.templates.template import Template @@ -24,7 +27,7 @@ class Meta: ordering = ['-order'] constraints = [ UniqueConstraint( - fields=['template', 'api_name'], + fields=['template', 'api_name', 'account'], condition=Q(is_deleted=False), name='processes_fieldtemplate_template_api_name_unique', ), @@ -58,6 +61,7 @@ class Meta: blank=True, related_name='fields', ) + # TODO Deprecated rules = models.ManyToManyField( 'processes.FieldsetTemplateRule', blank=True, @@ -113,3 +117,121 @@ class Meta: objects = BaseSoftDeleteManager.from_queryset( FieldTemplateValuesQuerySet, )() + + +class FieldTemplateRuleSet( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['order', 'id'] + constraints = [ + UniqueConstraint( + fields=['template', 'api_name', 'account'], + condition=Q(is_deleted=False), + name='fieldtemplateruleset_field_api_name_unique', + ), + ] + + api_name_prefix = 'field-ruleset' + template = models.ForeignKey( + Template, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='field_rulesets', + ) + field = models.ForeignKey( + FieldTemplate, + on_delete=models.CASCADE, + related_name='rulesets', + ) + type = models.CharField( + max_length=50, + choices=FieldRuleType.CHOICES, + ) + message = models.TextField( + null=True, + blank=True, + help_text='custom error message for a type="validator"', + ) + order = models.PositiveIntegerField(default=0) + + def __str__(self): + return self.api_name + + +class FieldTemplateRuleGroupOr( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + constraints = [ + UniqueConstraint( + fields=['api_name', 'template', 'account'], + condition=Q(is_deleted=False), + name='rulegroupor_field_rule_api_name_unique', + ), + ] + + api_name_prefix = 'field-rule-group-or' + template = models.ForeignKey( + Template, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='field_ruleset_groups_or', + ) + field_rule = models.ForeignKey( + FieldTemplateRuleSet, + on_delete=models.CASCADE, + related_name='groups_or', + ) + + def __str__(self): + return self.api_name + + +class FieldTemplateRuleGroupAnd( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + constraints = [ + UniqueConstraint( + fields=['template', 'api_name', 'account'], + condition=Q(is_deleted=False), + name='rulegroupand_group_or_api_name_unique', + ), + ] + + api_name_prefix = 'field-rule-group-and' + group_or = models.ForeignKey( + FieldTemplateRuleGroupOr, + on_delete=models.CASCADE, + related_name='groups_and', + ) + template = models.ForeignKey( + Template, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='field_rules_group_and', + ) + operator = models.CharField( + max_length=50, + choices=FieldRuleOperator.CHOICES, + ) + value = models.CharField(max_length=200, null=True, blank=True) + field = models.CharField( + max_length=200, + null=True, + ) + + def __str__(self): + return self.api_name diff --git a/backend/src/processes/models/templates/fieldset.py b/backend/src/processes/models/templates/fieldset.py index 0186d9962..690373a3c 100644 --- a/backend/src/processes/models/templates/fieldset.py +++ b/backend/src/processes/models/templates/fieldset.py @@ -3,17 +3,16 @@ from src.accounts.models import AccountBaseMixin from src.generics.managers import BaseSoftDeleteManager +from src.processes.enums import FieldSetRuleOperator from src.processes.models.base import BaseApiNameModel from src.processes.models.mixins import ( - BaseFieldSetMixin, - BaseFieldSetRuleMixin, + BaseFieldSetMixin, BaseFieldSetRuleMixin, ) from src.processes.models.templates.kickoff import Kickoff from src.processes.models.templates.template import Template from src.processes.models.templates.task import TaskTemplate from src.processes.querysets import ( - FieldsetTemplateQuerySet, - FieldsetTemplateRuleQuerySet, + FieldsetTemplateQuerySet, FieldsetTemplateRuleQuerySet, ) @@ -27,7 +26,7 @@ class Meta: ordering = ['-id'] constraints = [ UniqueConstraint( - fields=['api_name', 'template', 'is_shared'], + fields=['api_name', 'template', 'is_shared', 'account'], condition=Q(is_deleted=False), name='fieldsettemplate_template_api_name_is_shared_unique', ), @@ -82,6 +81,8 @@ class FieldsetTemplateRule( AccountBaseMixin, ): + # TODO Deprecated + class Meta: ordering = ['-id'] constraints = [ @@ -106,3 +107,118 @@ class Meta: def __str__(self): return self.name + + +class FieldSetTemplateRuleSet( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['order', 'id'] + constraints = [ + UniqueConstraint( + fields=['template', 'api_name', 'account'], + condition=Q(is_deleted=False), + name='fieldsetruleset_fieldset_api_name_unique', + ), + ] + + api_name_prefix = 'fieldset-ruleset' + template = models.ForeignKey( + Template, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='fieldset_rulesets', + ) + fieldset = models.ForeignKey( + FieldsetTemplate, + on_delete=models.CASCADE, + related_name='rulesets', + ) + message = models.TextField( + null=True, + blank=True, + help_text='custom error message for a type="validator"', + ) + order = models.PositiveIntegerField(default=0) + fields = models.ManyToManyField( + 'processes.FieldTemplate', + blank=True, + related_name='fieldset_rulesets', + ) + + def __str__(self): + return f'{self.type} / {self.api_name}' + + +class FieldSetTemplateRuleGroupOr( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + constraints = [ + UniqueConstraint( + fields=['api_name', 'template', 'account'], + condition=Q(is_deleted=False), + name='fieldset_rulegroupor_field_rule_api_name_unique', + ), + ] + + api_name_prefix = 'fieldset-rule-group-or' + template = models.ForeignKey( + Template, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='fieldset_ruleset_groups_or', + ) + fieldset_rule = models.ForeignKey( + FieldSetTemplateRuleSet, + on_delete=models.CASCADE, + related_name='groups_or', + ) + + def __str__(self): + return self.api_name + + +class FieldSetTemplateRuleGroupAnd( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + constraints = [ + UniqueConstraint( + fields=['template', 'api_name', 'account'], + condition=Q(is_deleted=False), + name='fieldset_rulegroupand_group_or_api_name_unique', + ), + ] + + api_name_prefix = 'fieldset-rule-group-and' + template = models.ForeignKey( + Template, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='fieldset_rules_group_and', + ) + group_or = models.ForeignKey( + FieldSetTemplateRuleGroupOr, + on_delete=models.CASCADE, + related_name='groups_and', + ) + operator = models.CharField( + max_length=50, + choices=FieldSetRuleOperator.CHOICES, + ) + value = models.CharField(max_length=200, null=True, blank=True) + + def __str__(self): + return f'{self.operator} {self.value}' diff --git a/backend/src/processes/models/workflows/fields.py b/backend/src/processes/models/workflows/fields.py index 45ee46fdc..92c324e86 100644 --- a/backend/src/processes/models/workflows/fields.py +++ b/backend/src/processes/models/workflows/fields.py @@ -4,6 +4,8 @@ from src.accounts.models import AccountBaseMixin from src.generics.managers import BaseSoftDeleteManager from src.generics.models import SoftDeleteModel +from src.processes.enums import FieldRuleType, FieldRuleOperator +from src.processes.models.base import BaseApiNameModel from src.processes.models.mixins import ( ApiNameMixin, FieldMixin, @@ -102,3 +104,92 @@ class Meta: def __str__(self): return self.value + + +class FieldRuleSet( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['order', 'id'] + + workflow = models.ForeignKey( + Workflow, + on_delete=models.CASCADE, + related_name='field_rulesets', + ) + field = models.ForeignKey( + TaskField, + on_delete=models.CASCADE, + related_name='rulesets', + ) + type = models.CharField( + max_length=50, + choices=FieldRuleType.CHOICES, + ) + message = models.TextField( + null=True, + blank=True, + help_text='custom error message for a type="validator"', + ) + order = models.PositiveIntegerField(default=0) + + def __str__(self): + return self.api_name + + +class FieldRuleGroupOr( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + + workflow = models.ForeignKey( + Workflow, + on_delete=models.CASCADE, + related_name='field_ruleset_groups_or', + ) + field_rule = models.ForeignKey( + FieldRuleSet, + on_delete=models.CASCADE, + related_name='groups_or', + ) + + def __str__(self): + return self.api_name + + +class FieldRuleGroupAnd( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + + api_name_prefix = 'field-rule-group-and' + group_or = models.ForeignKey( + FieldRuleGroupOr, + on_delete=models.CASCADE, + related_name='groups_and', + ) + workflow = models.ForeignKey( + Workflow, + on_delete=models.CASCADE, + related_name='field_rules_group_and', + ) + operator = models.CharField( + max_length=50, + choices=FieldRuleOperator.CHOICES, + ) + value = models.CharField(max_length=200, null=True, blank=True) + field = models.CharField( + max_length=200, + null=True, + ) + + def __str__(self): + return self.api_name diff --git a/backend/src/processes/models/workflows/fieldset.py b/backend/src/processes/models/workflows/fieldset.py index 86e08842e..84bca09dc 100644 --- a/backend/src/processes/models/workflows/fieldset.py +++ b/backend/src/processes/models/workflows/fieldset.py @@ -2,6 +2,9 @@ from src.accounts.models import AccountBaseMixin from src.generics.managers import BaseSoftDeleteManager +from src.processes.enums import ( + FieldSetRuleOperator, +) from src.processes.models.base import BaseApiNameModel from src.processes.models.mixins import ( BaseFieldSetMixin, @@ -10,7 +13,10 @@ from src.processes.models.workflows.kickoff import KickoffValue from src.processes.models.workflows.task import Task from src.processes.models.workflows.workflow import Workflow -from src.processes.querysets import FieldSetQuerySet, FieldSetRuleQuerySet +from src.processes.querysets import ( + FieldSetQuerySet, + FieldSetRuleQuerySet, +) class FieldSet( @@ -50,6 +56,7 @@ class FieldSetRule( BaseFieldSetRuleMixin, AccountBaseMixin, ): + # TODO Deprecated class Meta: ordering = ['-id'] @@ -61,3 +68,89 @@ class Meta: ) objects = BaseSoftDeleteManager.from_queryset(FieldSetRuleQuerySet)() + + +class FieldSetRuleSet( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['order', 'id'] + + workflow = models.ForeignKey( + Workflow, + on_delete=models.CASCADE, + related_name='fieldset_rulesets', + ) + fieldset = models.ForeignKey( + FieldSet, + on_delete=models.CASCADE, + related_name='rulesets', + ) + message = models.TextField( + null=True, + blank=True, + help_text='custom error message for a type="validator"', + ) + order = models.PositiveIntegerField(default=0) + fields = models.ManyToManyField( + 'processes.TaskField', + blank=True, + related_name='fieldset_rulesets', + ) + + def __str__(self): + return f'{self.type} / {self.api_name}' + + +class FieldSetRuleGroupOr( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + + api_name_prefix = 'fieldset-rule-group-or' + workflow = models.ForeignKey( + Workflow, + on_delete=models.CASCADE, + related_name='fieldset_ruleset_groups_or', + ) + fieldset_rule = models.ForeignKey( + FieldSetRuleSet, + on_delete=models.CASCADE, + related_name='groups_or', + ) + + def __str__(self): + return self.api_name + + +class FieldSetRuleGroupAnd( + BaseApiNameModel, + AccountBaseMixin, +): + + class Meta: + ordering = ['id'] + + workflow = models.ForeignKey( + Workflow, + on_delete=models.CASCADE, + related_name='fieldset_rules_group_and', + ) + group_or = models.ForeignKey( + FieldSetRuleGroupOr, + on_delete=models.CASCADE, + related_name='groups_and', + ) + operator = models.CharField( + max_length=50, + choices=FieldSetRuleOperator.CHOICES, + ) + value = models.CharField(max_length=200, null=True, blank=True) + + def __str__(self): + return f'{self.operator} {self.value}' diff --git a/backend/src/processes/models/workflows/task.py b/backend/src/processes/models/workflows/task.py index 9c641c8c8..94bfc9f5d 100644 --- a/backend/src/processes/models/workflows/task.py +++ b/backend/src/processes/models/workflows/task.py @@ -31,7 +31,9 @@ TaskRawPerformersMixin, ) from src.processes.models.workflows.workflow import Workflow -from src.processes.queries import GetTaskPerformersQuery +from src.processes.queries import ( + GetTaskPerformersQuery, +) from src.processes.querysets import ( DelayBaseQuerySet, TaskPerformerQuerySet, diff --git a/backend/src/processes/permissions.py b/backend/src/processes/permissions.py index 0c4bd5125..f2b818ffb 100644 --- a/backend/src/processes/permissions.py +++ b/backend/src/processes/permissions.py @@ -1,6 +1,5 @@ import re - from django.db.models import Q from rest_framework.permissions import BasePermission diff --git a/backend/src/processes/serializers/templates/field.py b/backend/src/processes/serializers/templates/field.py index 48ea10dff..05099cfbc 100644 --- a/backend/src/processes/serializers/templates/field.py +++ b/backend/src/processes/serializers/templates/field.py @@ -25,6 +25,9 @@ CreateOrUpdateRelatedMixin, CustomValidationApiNameMixin, ) +from src.processes.serializers.templates.field_rule import ( + FieldTemplateRuleSetSerializer, +) from src.processes.serializers.templates.selection import ( FieldTemplateSelectionSerializer, ) @@ -83,6 +86,7 @@ class Meta: 'default', 'dataset', 'order', + 'rulesets', ) create_or_update_fields = { 'type', @@ -111,6 +115,11 @@ class Meta: many=True, required=False, ) + rulesets = FieldTemplateRuleSetSerializer( + many=True, + required=False, + default=list, + ) def additional_validate(self, data: Dict[str, Any]): super().additional_validate(data) @@ -174,6 +183,18 @@ def create(self, validated_data: Dict[str, Any]): **self.context, }, ) + self.create_or_update_related( + data=validated_data.get('rulesets'), + ancestors_data={ + 'field': instance, + 'template': self.context['template'], + }, + slz_cls=FieldTemplateRuleSetSerializer, + slz_context={ + 'field': instance, + **self.context, + }, + ) return instance def update( @@ -212,6 +233,18 @@ def update( **self.context, }, ) + self.create_or_update_related( + data=validated_data.get('rulesets'), + ancestors_data={ + 'field': instance, + 'template': self.context['template'], + }, + slz_cls=FieldTemplateRuleSetSerializer, + slz_context={ + 'field': instance, + **self.context, + }, + ) return instance diff --git a/backend/src/processes/serializers/templates/field_rule.py b/backend/src/processes/serializers/templates/field_rule.py new file mode 100644 index 000000000..c62e76279 --- /dev/null +++ b/backend/src/processes/serializers/templates/field_rule.py @@ -0,0 +1,302 @@ +from typing import Any, Dict + +from rest_framework.fields import CharField +from rest_framework.serializers import ModelSerializer + +from src.generics.mixins.serializers import ( + AdditionalValidationMixin, + CustomValidationErrorMixin, +) +from src.processes.messages.template import ( + MSG_PT_0075, +) +from src.processes.models.templates.fields import ( + FieldTemplateRuleGroupAnd, + FieldTemplateRuleGroupOr, + FieldTemplateRuleSet, +) +from src.processes.serializers.templates.mixins import ( + CreateOrUpdateInstanceMixin, + CreateOrUpdateRelatedMixin, + CustomValidationApiNameMixin, +) + + +class FieldTemplateRuleGroupAndSerializer( + CreateOrUpdateInstanceMixin, + CustomValidationErrorMixin, + AdditionalValidationMixin, + CustomValidationApiNameMixin, + ModelSerializer, +): + + class Meta: + model = FieldTemplateRuleGroupAnd + api_primary_field = 'api_name' + fields = ( + 'api_name', + 'field', + 'operator', + 'value', + ) + create_or_update_fields = { + 'api_name', + 'field', + 'operator', + 'value', + 'group_or', + 'template', + 'account', + } + + api_name = CharField(max_length=200, required=False) + field = CharField(max_length=200) + + def create(self, validated_data: Dict[str, Any]): + self.additional_validate(validated_data) + task = self.context.get('task') + kickoff = self.context.get('kickoff') + field = self.context['field'] + task_name = 'Kickoff' if kickoff else task.name + return self.create_or_update_instance( + validated_data={ + 'template': self.context['template'], + 'account': self.context['account'], + 'group_or': self.context['group_or'], + 'field': field, + **validated_data, + }, + not_unique_exception_msg=MSG_PT_0075( + task_name=task_name, + field_name=field.name, + api_name=validated_data.get('api_name'), + ), + ) + + def update( + self, + instance: FieldTemplateRuleGroupAnd, + validated_data: Dict[str, Any], + ): + self.additional_validate(validated_data) + task = self.context.get('task') + kickoff = self.context.get('kickoff') + field = self.context['field'] + task_name = 'Kickoff' if kickoff else task.name + return self.create_or_update_instance( + instance=instance, + validated_data={ + 'template': self.context['template'], + 'account': self.context['account'], + 'group_or': self.context['group_or'], + 'field': field, + **validated_data, + }, + not_unique_exception_msg=MSG_PT_0075( + task_name=task_name, + field_name=field.name, + api_name=validated_data.get('api_name'), + ), + ) + + +class FieldTemplateRuleGroupOrSerializer( + CreateOrUpdateRelatedMixin, + CreateOrUpdateInstanceMixin, + CustomValidationErrorMixin, + AdditionalValidationMixin, + CustomValidationApiNameMixin, + ModelSerializer, +): + + class Meta: + model = FieldTemplateRuleGroupOr + api_primary_field = 'api_name' + fields = ( + 'api_name', + 'groups_and', + ) + create_or_update_fields = { + 'api_name', + 'field_rule', + 'template', + 'account', + } + + api_name = CharField(max_length=200, required=False) + groups_and = FieldTemplateRuleGroupAndSerializer(many=True) + + def create(self, validated_data: Dict[str, Any]): + self.additional_validate(validated_data) + task = self.context.get('task') + kickoff = self.context.get('kickoff') + field = self.context['field'] + task_name = 'Kickoff' if kickoff else task.name + instance = self.create_or_update_instance( + validated_data={ + 'template': self.context['template'], + 'account': self.context['account'], + 'field_rule': self.context['field_rule'], + **validated_data, + }, + not_unique_exception_msg=MSG_PT_0075( + task_name=task_name, + field_name=field.name, + api_name=validated_data.get('api_name'), + ), + ) + self.create_or_update_related( + data=validated_data.pop('groups_and', None), + slz_cls=FieldTemplateRuleGroupAndSerializer, + ancestors_data={ + 'group_or': instance, + 'template': self.context['template'], + }, + slz_context={ + 'group_or': instance, + **self.context, + }, + ) + return instance + + def update( + self, + instance: FieldTemplateRuleGroupOr, + validated_data: Dict[str, Any], + ): + self.additional_validate(validated_data) + task = self.context.get('task') + kickoff = self.context.get('kickoff') + field = self.context['field'] + task_name = 'Kickoff' if kickoff else task.name + instance = self.create_or_update_instance( + instance=instance, + validated_data={ + 'template': self.context['template'], + 'account': self.context['account'], + 'field_rule': self.context['field_rule'], + **validated_data, + }, + not_unique_exception_msg=MSG_PT_0075( + task_name=task_name, + field_name=field.name, + api_name=validated_data.get('api_name'), + ), + ) + self.create_or_update_related( + data=validated_data.pop('groups_and', None), + slz_cls=FieldTemplateRuleGroupAndSerializer, + ancestors_data={ + 'group_or': instance, + 'template': self.context['template'], + }, + slz_context={ + 'group_or': instance, + **self.context, + }, + ) + return instance + + +class FieldTemplateRuleSetSerializer( + CreateOrUpdateRelatedMixin, + CreateOrUpdateInstanceMixin, + CustomValidationErrorMixin, + AdditionalValidationMixin, + CustomValidationApiNameMixin, + ModelSerializer, +): + + class Meta: + model = FieldTemplateRuleSet + api_primary_field = 'api_name' + fields = ( + 'api_name', + 'type', + 'message', + 'order', + 'groups_or', + ) + create_or_update_fields = { + 'api_name', + 'type', + 'message', + 'order', + 'field', + 'template', + 'account', + } + + api_name = CharField(max_length=200, required=False) + groups_or = FieldTemplateRuleGroupOrSerializer(many=True) + + def create(self, validated_data: Dict[str, Any]): + self.additional_validate(validated_data) + task = self.context.get('task') + kickoff = self.context.get('kickoff') + field = self.context['field'] + task_name = 'Kickoff' if kickoff else task.name + instance = self.create_or_update_instance( + validated_data={ + 'template': self.context['template'], + 'account': self.context['account'], + 'field': field, + **validated_data, + }, + not_unique_exception_msg=MSG_PT_0075( + task_name=task_name, + field_name=field.name, + api_name=validated_data.get('api_name'), + ), + ) + self.create_or_update_related( + data=validated_data.get('groups_or'), + slz_cls=FieldTemplateRuleGroupOrSerializer, + ancestors_data={ + 'field_rule': instance, + 'template': self.context['template'], + }, + slz_context={ + 'field_rule': instance, + **self.context, + }, + ) + return instance + + def update( + self, + instance: FieldTemplateRuleSet, + validated_data: Dict[str, Any], + ): + self.additional_validate(validated_data) + task = self.context.get('task') + kickoff = self.context.get('kickoff') + field = self.context['field'] + task_name = 'Kickoff' if kickoff else task.name + instance = self.create_or_update_instance( + instance=instance, + validated_data={ + 'template': self.context['template'], + 'account': self.context['account'], + 'field': field, + **validated_data, + }, + not_unique_exception_msg=MSG_PT_0075( + task_name=task_name, + field_name=field.name, + api_name=validated_data.get('api_name'), + ), + ) + self.create_or_update_related( + data=validated_data.get('groups_or'), + slz_cls=FieldTemplateRuleGroupOrSerializer, + ancestors_data={ + 'field_rule': instance, + 'template': self.context['template'], + }, + slz_context={ + 'field_rule': instance, + **self.context, + }, + ) + return instance diff --git a/backend/src/processes/serializers/templates/fieldset.py b/backend/src/processes/serializers/templates/fieldset.py index d8ffcfa7c..b114cc02e 100644 --- a/backend/src/processes/serializers/templates/fieldset.py +++ b/backend/src/processes/serializers/templates/fieldset.py @@ -2,40 +2,20 @@ from rest_framework.fields import CharField, ChoiceField, IntegerField from rest_framework.serializers import ModelSerializer, SerializerMethodField from src.generics.fields import ( - RelatedApiNameListField, AccountPrimaryKeyRelatedField, + AccountPrimaryKeyRelatedField, ) from src.generics.mixins.serializers import CustomValidationErrorMixin from src.processes.enums import FieldSetLayout, LabelPosition from src.processes.models.templates.fieldset import ( FieldsetTemplate, - FieldsetTemplateRule, ) from src.processes.models.templates.template import Template from src.processes.serializers.templates.field import ( FieldTemplateSerializer, ) - - -class FieldsetTemplateRuleSerializer( - CustomValidationErrorMixin, - ModelSerializer, -): - - class Meta: - model = FieldsetTemplateRule - fields = ( - 'type', - 'value', - 'api_name', - 'fields', - ) - - api_name = CharField(required=False, max_length=200) - fields = RelatedApiNameListField( - required=False, - allow_empty=True, - default=list, - ) +from src.processes.serializers.templates.fieldset_rule import ( + FieldSetTemplateRuleSetSerializer, +) class FieldsetTemplateSerializer( @@ -54,7 +34,7 @@ class Meta: 'name', 'label_position', 'layout', - 'rules', + 'rulesets', 'fields', ) @@ -72,7 +52,7 @@ class Meta: choices=FieldSetLayout.CHOICES, required=False, ) - rules = FieldsetTemplateRuleSerializer( + rulesets = FieldSetTemplateRuleSetSerializer( many=True, required=False, default=list, @@ -105,12 +85,12 @@ class Meta: 'name', 'label_position', 'layout', - 'rules', + 'rulesets', 'fields', 'usage', ) - rules = FieldsetTemplateRuleSerializer( + rulesets = FieldSetTemplateRuleSetSerializer( many=True, required=False, default=list, diff --git a/backend/src/processes/serializers/templates/fieldset_rule.py b/backend/src/processes/serializers/templates/fieldset_rule.py new file mode 100644 index 000000000..da1ef30ce --- /dev/null +++ b/backend/src/processes/serializers/templates/fieldset_rule.py @@ -0,0 +1,68 @@ +from rest_framework.fields import CharField +from rest_framework.serializers import ModelSerializer + +from src.generics.fields import ( + RelatedApiNameListField, +) +from src.generics.mixins.serializers import ( + CustomValidationErrorMixin, +) +from src.processes.models.templates.fieldset import ( + FieldSetTemplateRuleGroupAnd, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleSet, +) + + +class FieldSetTemplateRuleGroupAndSerializer( + CustomValidationErrorMixin, + ModelSerializer, +): + + class Meta: + model = FieldSetTemplateRuleGroupAnd + fields = ( + 'api_name', + 'operator', + 'value', + ) + + api_name = CharField(max_length=200, required=False) + + +class FieldSetTemplateRuleGroupOrSerializer( + CustomValidationErrorMixin, + ModelSerializer, +): + + class Meta: + model = FieldSetTemplateRuleGroupOr + fields = ( + 'api_name', + 'groups_and', + ) + + api_name = CharField(max_length=200, required=False) + groups_and = FieldSetTemplateRuleGroupAndSerializer(many=True) + + +class FieldSetTemplateRuleSetSerializer( + CustomValidationErrorMixin, + ModelSerializer, +): + + class Meta: + model = FieldSetTemplateRuleSet + fields = ( + 'api_name', + 'message', + 'order', + 'fields', + 'groups_or', + ) + + api_name = CharField(max_length=200, required=False) + fields = RelatedApiNameListField( + default=list, + ) + groups_or = FieldSetTemplateRuleGroupOrSerializer(many=True) diff --git a/backend/src/processes/serializers/templates/mixins.py b/backend/src/processes/serializers/templates/mixins.py index cf372b620..6ac0b822d 100644 --- a/backend/src/processes/serializers/templates/mixins.py +++ b/backend/src/processes/serializers/templates/mixins.py @@ -265,10 +265,10 @@ def create_or_update_fieldsets( """ Sync fieldsets for kickoff/task when activating a template. Existing fieldsets are matched by api_name and partially updated. - New ones with non-empty fields/rules are created as-is (preserves - draft api_names); compact shared_fieldset_id references are cloned - via create_from_shared. Fieldsets missing from the payload are - deleted. """ + New ones with non-empty fields/rulesets are created as-is + (preserves draft api_names); compact shared_fieldset_id + references are cloned via create_from_shared. Fieldsets missing + from the payload are deleted. """ instance = task or kickoff existing_fieldsets = {f.api_name: f for f in instance.fieldsets.all()} @@ -303,12 +303,12 @@ def create_or_update_fieldsets( # — create as-is to preserve field/rule api_names. # Otherwise clone from shared. try: - # validated_data always has fields/rules defaults ([]). + # validated_data always has fields/rulesets defaults ([]). # Non-empty means payload is expanded — create as-is. # Otherwise clone from shared (compact reference). if ( fieldset_data.get('fields') - or fieldset_data.get('rules') + or fieldset_data.get('rulesets') ): fieldset = service.create( name=( @@ -333,7 +333,7 @@ def create_or_update_fieldsets( or shared_fieldset.layout ), fields=fieldset_data.get('fields') or [], - rules=fieldset_data.get('rules') or [], + rulesets=fieldset_data.get('rulesets') or [], order=fieldset_data['order'], is_shared=False, shared_fieldset_id=shared_fieldset.id, diff --git a/backend/src/processes/services/fieldsets/fieldset.py b/backend/src/processes/services/fieldsets/fieldset.py index 0f0782d1a..d19bdef02 100644 --- a/backend/src/processes/services/fieldsets/fieldset.py +++ b/backend/src/processes/services/fieldsets/fieldset.py @@ -7,7 +7,6 @@ from src.generics.base.service import BaseModelService from src.processes.enums import LabelPosition, FieldSetLayout from src.processes.messages.fieldset import ( - MSG_FS_0014, MSG_FS_0015, MSG_FS_0016, ) @@ -17,7 +16,9 @@ ) from src.processes.models.templates.fieldset import ( FieldsetTemplate, - FieldsetTemplateRule, + FieldSetTemplateRuleSet, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleGroupAnd, ) from src.processes.services.exceptions import ( FieldsetTemplateInUseException, @@ -25,12 +26,11 @@ FieldsetTemplateSharedIdMissing, FieldsetTemplateTemplateIdMissing, ) +from src.processes.services.fieldsets.fieldset_rule import \ + FieldsetTemplateRuleSetService from src.processes.services.templates.field_template import ( FieldTemplateService, ) -from src.processes.services.fieldsets.fieldset_rule import ( - FieldsetTemplateRuleService, -) from src.processes.utils.common import create_api_name from src.utils.validation import raise_validation_error @@ -139,14 +139,14 @@ def create_from_shared( def _create_related( self, - rules: Optional[List[Dict]] = None, + rulesets: Optional[List[Dict]] = None, fields: Optional[List[Dict]] = None, **kwargs, ): if fields: self._create_fields(fields_data=fields) - if rules: - self.create_rules(rules_data=rules) + if rulesets: + self.create_rulesets(rulesets_data=rulesets) def _get_step_name(self) -> str: if self.instance.kickoff_id: @@ -160,6 +160,10 @@ def _create_fields( fields_data: List[Dict], ): for field_data in fields_data: + field_data_dict = dict(field_data) + # Field-level rulesets are not created here yet. + field_data_dict.pop('rulesets', None) + field_data_dict.pop('id', None) service = FieldTemplateService( user=self.user, is_superuser=self.is_superuser, @@ -170,7 +174,7 @@ def _create_fields( service.create( fieldset_id=self.instance.id, template_id=self.instance.template_id, - **field_data, + **field_data_dict, ) except IntegrityError as ex: ex_str = str(ex) @@ -179,12 +183,12 @@ def _create_fields( 'processes_fieldtemplate_template_api_name_unique' in ex_str ): - api_name = field_data.get('api_name') + api_name = field_data_dict.get('api_name') raise_validation_error( api_name=api_name, message=MSG_FS_0015( name=step_name, - field_name=field_data.get('name'), + field_name=field_data_dict.get('name'), api_name=api_name, ), ) @@ -193,7 +197,7 @@ def _create_fields( '_template_api_name_unique' in ex_str ): - selections = field_data.get('selections') or [] + selections = field_data_dict.get('selections') or [] api_names = [s.get('api_name') for s in selections] api_name = next( ( @@ -248,16 +252,6 @@ def _update_fields( self.instance.fields.exclude(api_name__in=fields_api_names).delete() - def _validate_rules(self): - for rule in self.instance.rules.all(): - service = FieldsetTemplateRuleService( - user=self.user, - is_superuser=self.is_superuser, - auth_type=self.auth_type, - instance=rule, - ) - service._validate() - def partial_update( self, **update_kwargs, @@ -266,7 +260,7 @@ def partial_update( if self.instance.is_shared and self.instance.child_fieldsets.exists(): raise FieldsetTemplateInUseException2 - rules_data = update_kwargs.pop('rules', None) + rulesets_data = update_kwargs.pop('rulesets', None) fields_data = update_kwargs.pop('fields', None) with transaction.atomic(): if update_kwargs: @@ -277,9 +271,8 @@ def partial_update( if fields_data is not None: self._update_fields(fields_data=fields_data) - if rules_data is not None: - self.update_rules(rules_data=rules_data) - self._validate_rules() + if rulesets_data is not None: + self.update_rulesets(rulesets_data=rulesets_data) return self.instance def partial_update_instance( @@ -325,16 +318,24 @@ def _replace_api_names(shared_fieldset_data: dict) -> dict: fieldset_data['fields'] = updated_fields_data updated_rules_data = [] - for rule_data in fieldset_data.get('rules', []): - rule_data['api_name'] = create_api_name( - FieldsetTemplateRule.api_name_prefix, + for ruleset_data in fieldset_data.get('rulesets', []): + ruleset_data['api_name'] = create_api_name( + FieldSetTemplateRuleSet.api_name_prefix, ) - rule_data['fields'] = [ + for group_or_data in ruleset_data.get('groups_or', []): + group_or_data['api_name'] = create_api_name( + FieldSetTemplateRuleGroupOr.api_name_prefix, + ) + for group_and_data in group_or_data.get('groups_and', []): + group_and_data['api_name'] = create_api_name( + FieldSetTemplateRuleGroupAnd.api_name_prefix, + ) + ruleset_data['fields'] = [ fields_map[old_api_name] - for old_api_name in rule_data.get('fields', []) + for old_api_name in ruleset_data.get('fields', []) ] - updated_rules_data.append(rule_data) - fieldset_data['rules'] = updated_rules_data + updated_rules_data.append(ruleset_data) + fieldset_data['rulesets'] = updated_rules_data return fieldset_data def get_new_fieldset_data( @@ -353,85 +354,61 @@ def get_new_fieldset_data( if description is not None: fieldset_data['description'] = description fieldset_data.pop('order', None) + fieldset_data.pop('id', None) return fieldset_data - def _raise_duplicate_rule_api_name(self, api_name: Optional[str]): - raise_validation_error( - api_name=api_name, - message=MSG_FS_0014( - name=self._get_step_name(), - api_name=api_name, - ), - ) - - def create_rules( + def create_ruleset( self, - rules_data: List[Dict], - ): - service = FieldsetTemplateRuleService( + ruleset_data: dict, + ) -> FieldSetTemplateRuleSet: + + service = FieldsetTemplateRuleSetService( user=self.user, is_superuser=self.is_superuser, auth_type=self.auth_type, ) - for rule_data in rules_data: - api_name = rule_data.get('api_name') - if ( - api_name - and self.instance.template_id - and FieldsetTemplateRule.objects.filter( - api_name=api_name, - fieldset__template_id=self.instance.template_id, - is_deleted=False, - ).exists() - ): - self._raise_duplicate_rule_api_name(api_name) - try: - service.create( - fieldset_id=self.instance.id, - **rule_data, - ) - except IntegrityError as ex: - if ( - 'fieldsettemplate_rule_api_name_template_unique' - not in str(ex) - ): - raise - self._raise_duplicate_rule_api_name(api_name) + return service.create( + fieldset_id=self.instance.id, + **ruleset_data, + ) - def update_rules( + def create_rulesets( self, - rules_data: List[Dict], + rulesets_data: List[Dict], ): - """ All dataset items will be updated """ + for ruleset_data in rulesets_data: + self.create_ruleset(ruleset_data) - existing_rules = { - rule.api_name: rule for rule in self.instance.rules.all() + def update_rulesets( + self, + rulesets_data: List[Dict], + ): + """ All rulesets will be updated """ + + existing_rulesets = { + ruleset.api_name: ruleset + for ruleset in self.instance.rulesets.all() } - rule_api_names = set() - for rule_data in rules_data: - rule_api_name = rule_data.get('api_name') - if rule_api_name and rule_api_name in existing_rules: - service = FieldsetTemplateRuleService( + ruleset_api_names = set() + for ruleset_data in rulesets_data: + ruleset_data_dict = dict(ruleset_data) + ruleset_api_name = ruleset_data_dict.get('api_name') + if ruleset_api_name and ruleset_api_name in existing_rulesets: + service = FieldsetTemplateRuleSetService( user=self.user, is_superuser=self.is_superuser, auth_type=self.auth_type, - instance=existing_rules[rule_api_name], + instance=existing_rulesets[ruleset_api_name], ) - service.partial_update(**rule_data) - rule_api_names.add(rule_api_name) + service.partial_update(**ruleset_data) + ruleset_api_names.add(ruleset_api_name) else: - service = FieldsetTemplateRuleService( - user=self.user, - is_superuser=self.is_superuser, - auth_type=self.auth_type, - ) - rule = service.create( - fieldset_id=self.instance.id, - **rule_data, - ) - rule_api_names.add(rule.api_name) + ruleset = self.create_ruleset(ruleset_data) + ruleset_api_names.add(ruleset.api_name) - self.instance.rules.exclude(api_name__in=rule_api_names).delete() + self.instance.rulesets.exclude( + api_name__in=ruleset_api_names, + ).delete() @staticmethod def to_json(fieldset: FieldsetTemplate) -> dict: diff --git a/backend/src/processes/services/fieldsets/fieldset_rule.py b/backend/src/processes/services/fieldsets/fieldset_rule.py index 2c4ccfe04..cce0f09d9 100644 --- a/backend/src/processes/services/fieldsets/fieldset_rule.py +++ b/backend/src/processes/services/fieldsets/fieldset_rule.py @@ -1,29 +1,39 @@ from decimal import Decimal, DecimalException -from typing import List, Optional +from typing import Dict, List, Optional + from django.contrib.auth import get_user_model from django.db import transaction + from src.generics.base.service import BaseModelService -from src.processes.enums import FieldSetRuleType, FieldType +from src.processes.enums import ( + FieldSetRuleOperator, + FieldType, +) +from src.processes.messages.fieldset import MSG_FS_0005 from src.processes.models.templates.fields import FieldTemplate from src.processes.models.templates.fieldset import ( - FieldsetTemplateRule, + FieldSetTemplateRuleGroupAnd, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleSet, ) from src.processes.services.exceptions import ( + FieldsetTemplateRuleServiceException, FieldsetTemplateRuleSumMaxFieldsNotNumber, FieldsetTemplateRuleSumMaxInvalidValue, - FieldsetTemplateRuleServiceException, ) -from src.processes.messages.fieldset import MSG_FS_0005 UserModel = get_user_model() -class FieldsetTemplateRuleService(BaseModelService): +class FieldsetTemplateRuleSetService(BaseModelService): - def _validate_sum_equal(self, **kwargs) -> Decimal: + def _validate_sum( + self, + group_and: FieldSetTemplateRuleGroupAnd, + ) -> Decimal: - value = self.instance.value + value = group_and.value if not value: raise FieldsetTemplateRuleSumMaxInvalidValue try: @@ -35,30 +45,35 @@ def _validate_sum_equal(self, **kwargs) -> Decimal: raise FieldsetTemplateRuleSumMaxFieldsNotNumber return result - def _validate(self, **kwargs): - - """ Call after objects save """ + def _validate( + self, + group_and: FieldSetTemplateRuleGroupAnd, + ): - validator = getattr(self, f'_validate_{self.instance.type}', None) - validator(**kwargs) + if group_and.operator in FieldSetRuleOperator.SUM_OPERATORS: + self._validate_sum(group_and=group_and) def _create_instance( self, - type: FieldSetRuleType.LITERALS, # noqa: A002 - value: Optional[str] = None, - fieldset_id: Optional[int] = None, + fieldset_id: int, api_name: Optional[str] = None, + message: Optional[str] = None, + order: int = 0, + template_id: Optional[int] = None, **kwargs, ): + create_kwargs = { 'account': self.account, 'type': type, - 'value': value, + 'message': message, + 'order': order, 'fieldset_id': fieldset_id, + 'template_id': template_id, } if api_name: create_kwargs['api_name'] = api_name - self.instance = FieldsetTemplateRule.objects.create(**create_kwargs) + self.instance = FieldSetTemplateRuleSet.objects.create(**create_kwargs) return self.instance def _create_related( @@ -66,8 +81,11 @@ def _create_related( **kwargs, ): fields = kwargs.pop('fields', None) + groups_or = kwargs.pop('groups_or', None) if fields is not None: - self._set_fields(fields) + self._set_fields(fields_api_names=fields) + if groups_or is not None: + self._set_groups_or(groups_or_data=groups_or) def _get_valid_fields( self, @@ -97,28 +115,160 @@ def _get_valid_fields( def _set_fields(self, fields_api_names: List[str], **kwargs): if fields_api_names: - fields = self._get_valid_fields(fields_api_names, **kwargs) + fields = self._get_valid_fields( + fields_api_names=fields_api_names, + **kwargs, + ) self.instance.fields.set(fields) else: self.instance.fields.clear() + def _create_group_and( + self, + group_or: FieldSetTemplateRuleGroupOr, + group_and_data: Dict, + ) -> FieldSetTemplateRuleGroupAnd: + create_kwargs = { + 'group_or': group_or, + 'account': self.account, + 'template_id': self.instance.template_id, + 'operator': group_and_data['operator'], + 'value': group_and_data.get('value'), + } + api_name = group_and_data.get('api_name') + if api_name: + create_kwargs['api_name'] = api_name + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + **create_kwargs, + ) + self._validate(group_and=group_and) + return group_and + + def _update_group_and( + self, + group_and: FieldSetTemplateRuleGroupAnd, + group_and_data: Dict, + ) -> FieldSetTemplateRuleGroupAnd: + update_fields = [] + if 'operator' in group_and_data: + group_and.operator = group_and_data['operator'] + update_fields.append('operator') + if 'value' in group_and_data: + group_and.value = group_and_data['value'] + update_fields.append('value') + if update_fields: + group_and.save(update_fields=update_fields) + self._validate(group_and=group_and) + return group_and + + def _set_groups_and( + self, + group_or: FieldSetTemplateRuleGroupOr, + groups_and_data: List[Dict], + ): + existing_groups_and = { + group_and.api_name: group_and + for group_and in group_or.groups_and.all() + } + groups_and_api_names = set() + for group_and_data in groups_and_data: + group_and_data_dict = dict(group_and_data) + api_name = group_and_data_dict.get('api_name') + if api_name and api_name in existing_groups_and: + group_and = self._update_group_and( + group_and=existing_groups_and[api_name], + group_and_data=group_and_data_dict, + ) + else: + group_and = self._create_group_and( + group_or=group_or, + group_and_data=group_and_data_dict, + ) + groups_and_api_names.add(group_and.api_name) + group_or.groups_and.exclude( + api_name__in=groups_and_api_names, + ).delete() + + def _create_group_or( + self, + group_or_data: Dict, + ) -> FieldSetTemplateRuleGroupOr: + group_or_data_dict = dict(group_or_data) + groups_and_data = group_or_data_dict.pop('groups_and', None) + create_kwargs = { + 'fieldset_rule': self.instance, + 'account': self.account, + 'template_id': self.instance.template_id, + } + api_name = group_or_data_dict.get('api_name') + if api_name: + create_kwargs['api_name'] = api_name + group_or = FieldSetTemplateRuleGroupOr.objects.create(**create_kwargs) + if groups_and_data is not None: + for group_and_data in groups_and_data: + self._create_group_and( + group_or=group_or, + group_and_data=group_and_data, + ) + return group_or + + def _update_group_or( + self, + group_or: FieldSetTemplateRuleGroupOr, + group_or_data: Dict, + ) -> FieldSetTemplateRuleGroupOr: + groups_and_data = group_or_data.get('groups_and') + if groups_and_data is not None: + self._set_groups_and( + group_or=group_or, + groups_and_data=groups_and_data, + ) + return group_or + + def _set_groups_or(self, groups_or_data: List[Dict]): + existing_groups_or = { + group_or.api_name: group_or + for group_or in self.instance.groups_or.all() + } + groups_or_api_names = set() + for group_or_data in groups_or_data: + group_or_data_dict = dict(group_or_data) + api_name = group_or_data_dict.get('api_name') + if api_name and api_name in existing_groups_or: + group_or = self._update_group_or( + group_or=existing_groups_or[api_name], + group_or_data=group_or_data_dict, + ) + else: + group_or = self._create_group_or( + group_or_data=group_or_data_dict, + ) + groups_or_api_names.add(group_or.api_name) + self.instance.groups_or.exclude( + api_name__in=groups_or_api_names, + ).delete() + def create( self, **kwargs, - ) -> FieldsetTemplateRule: + ) -> FieldSetTemplateRuleSet: with transaction.atomic(): self._create_instance(**kwargs) self._create_related(**kwargs) self._create_actions(**kwargs) - self._validate(**kwargs) return self.instance - def partial_update(self, **update_kwargs) -> FieldsetTemplateRule: + def partial_update(self, **update_kwargs) -> FieldSetTemplateRuleSet: fields = update_kwargs.pop('fields', None) + groups_or = update_kwargs.pop('groups_or', None) with transaction.atomic(): - result = super().partial_update(**update_kwargs, force_save=True) + result = super().partial_update( + force_save=True, + **update_kwargs, + ) if fields is not None: - self._set_fields(fields) - self._validate(**update_kwargs) + self._set_fields(fields_api_names=fields) + if groups_or is not None: + self._set_groups_or(groups_or_data=groups_or) return result diff --git a/backend/src/processes/services/tasks/field.py b/backend/src/processes/services/tasks/field.py index aed550452..f3b7f17ff 100644 --- a/backend/src/processes/services/tasks/field.py +++ b/backend/src/processes/services/tasks/field.py @@ -351,7 +351,7 @@ def _create_related( self._link_new_attachments(raw_value) elif self.instance.type in FieldType.TYPES_WITH_SELECTIONS: self._create_selections(instance_template) - if instance_template.rules.all().exists(): + if instance_template.rulesets.all().exists(): self._link_rules(instance_template, **kwargs) def _link_new_attachments( @@ -389,14 +389,14 @@ def _link_rules( ): rule_api_names = set( - instance_template.rules.values_list('api_name', flat=True), + instance_template.rulesets.values_list('api_name', flat=True), ) rules = FieldSetRule.objects.filter( account=self.account, fieldset_id=kwargs['fieldset_id'], api_name__in=rule_api_names, ) - self.instance.rules.set(rules) + self.instance.rulesets.set(rules) def _remove_unused_attachments( self, diff --git a/backend/src/processes/services/tasks/mixins.py b/backend/src/processes/services/tasks/mixins.py index a90a1700e..ca3579cf5 100644 --- a/backend/src/processes/services/tasks/mixins.py +++ b/backend/src/processes/services/tasks/mixins.py @@ -13,7 +13,7 @@ class ConditionMixin: @staticmethod - def create_rules( + def create_rulesets( conditions, conditions_tree, ): diff --git a/backend/src/processes/services/tasks/task.py b/backend/src/processes/services/tasks/task.py index 154340269..a6f75660a 100644 --- a/backend/src/processes/services/tasks/task.py +++ b/backend/src/processes/services/tasks/task.py @@ -211,7 +211,7 @@ def create_conditions_from_template( )) conditions_tree[template.api_name] = rules_tree conditions = Condition.objects.bulk_create(conditions) - self.create_rules(conditions, conditions_tree) + self.create_rulesets(conditions, conditions_tree) def create_fields_from_template(self, instance_template: TaskTemplate): active_fieldset_ids = ( diff --git a/backend/src/processes/services/tasks/task_version.py b/backend/src/processes/services/tasks/task_version.py index ed4e75762..a1a628e49 100644 --- a/backend/src/processes/services/tasks/task_version.py +++ b/backend/src/processes/services/tasks/task_version.py @@ -166,7 +166,7 @@ def _update_conditions( )) conditions_tree[condition_data['api_name']] = rules_tree conditions = Condition.objects.bulk_create(conditions) - self.create_rules(conditions, conditions_tree) + self.create_rulesets(conditions, conditions_tree) def _update_field( self, @@ -233,7 +233,7 @@ def _update_fieldset_rules( }, ) rule_ids.append(rule.id) - fieldset.rules.exclude(id__in=rule_ids).delete() + fieldset.rulesets.exclude(id__in=rule_ids).delete() def _update_field_rules( self, @@ -249,9 +249,9 @@ def _update_field_rules( fieldset=fieldset, api_name__in=rules_api_names, ) - field.rules.set(rules) + field.rulesets.set(rules) else: - field.rules.clear() + field.rulesets.clear() def _update_fieldset_fields( self, diff --git a/backend/src/processes/services/workflows/fieldsets/fieldset.py b/backend/src/processes/services/workflows/fieldsets/fieldset.py index 39943f1c5..365c28457 100644 --- a/backend/src/processes/services/workflows/fieldsets/fieldset.py +++ b/backend/src/processes/services/workflows/fieldsets/fieldset.py @@ -66,7 +66,8 @@ def _create_fields( ) def _create_rules(self, instance_template, **kwargs): - for rule_template in instance_template.rules.filter(is_deleted=False): + ruleset_templates = instance_template.rulesets.filter(is_deleted=False) + for rule_template in ruleset_templates: service = FieldSetRuleService(user=self.user) service.create( instance_template=rule_template, @@ -79,7 +80,7 @@ def _create_related(self, instance_template, **kwargs): self._create_fields(instance_template, **kwargs) def validate_rules(self) -> bool: - rules = list(self.instance.rules.order_by('type').all()) + rules = list(self.instance.rulesets.order_by('type').all()) for _, group in groupby(rules, key=lambda r: r.type): group_rules = list(group) if len(group_rules) == 1: diff --git a/backend/src/processes/services/workflows/kickoff_version.py b/backend/src/processes/services/workflows/kickoff_version.py index 1cd91f8e3..d40fee6fb 100644 --- a/backend/src/processes/services/workflows/kickoff_version.py +++ b/backend/src/processes/services/workflows/kickoff_version.py @@ -116,7 +116,7 @@ def _update_fieldset_rules( }, ) rule_ids.append(rule.id) - fieldset.rules.exclude(id__in=rule_ids).delete() + fieldset.rulesets.exclude(id__in=rule_ids).delete() def _update_field_rules( self, @@ -132,9 +132,9 @@ def _update_field_rules( fieldset=fieldset, api_name__in=rules_api_names, ) - field.rules.set(rules) + field.rulesets.set(rules) else: - field.rules.clear() + field.rulesets.clear() def _update_fieldset_fields( self, diff --git a/backend/src/processes/tests/fixtures.py b/backend/src/processes/tests/fixtures.py index b6e9222ea..ad9a26ce1 100644 --- a/backend/src/processes/tests/fixtures.py +++ b/backend/src/processes/tests/fixtures.py @@ -25,7 +25,10 @@ from src.payment.enums import BillingPeriod from src.processes.enums import ( ConditionAction, + FieldRuleOperator, + FieldRuleType, FieldSetLayout, + FieldSetRuleOperator, FieldType, LabelPosition, OwnerRole, @@ -36,7 +39,7 @@ TaskStatus, TemplateType, WorkflowEventType, - WorkflowStatus, FieldSetRuleType, + WorkflowStatus, ) from src.processes.models.templates.checklist import ( ChecklistTemplate, @@ -49,10 +52,17 @@ ) from src.processes.models.templates.fieldset import ( FieldsetTemplate, - FieldsetTemplateRule, + FieldSetTemplateRuleSet, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleGroupAnd, +) +from src.processes.models.templates.fields import ( + FieldTemplate, + FieldTemplateRuleSet, + FieldTemplateRuleGroupOr, + FieldTemplateRuleGroupAnd, + FieldTemplateSelection, ) -from src.processes.models.templates.fields import FieldTemplate, \ - FieldTemplateSelection from src.processes.models.templates.kickoff import Kickoff from src.processes.models.templates.owner import TemplateOwner from src.processes.models.templates.preset import ( @@ -72,7 +82,12 @@ from src.processes.models.workflows.fields import ( TaskField, ) -from src.processes.models.workflows.fieldset import FieldSet, FieldSetRule +from src.processes.models.workflows.fieldset import ( + FieldSet, + FieldSetRuleSet, + FieldSetRuleGroupOr, + FieldSetRuleGroupAnd, +) from src.processes.models.workflows.kickoff import KickoffValue from src.processes.models.workflows.task import Task from src.processes.models.workflows.workflow import Workflow @@ -893,9 +908,14 @@ def create_test_shared_fieldset( order: int = 0, label_position: LabelPosition.LITERALS = LabelPosition.TOP, layout: FieldSetLayout.LITERALS = FieldSetLayout.VERTICAL, - rule_type: Optional[FieldSetRuleType.LITERALS] = None, + rule_operator: Optional[FieldSetRuleOperator.LITERALS] = None, rule_value: Optional[str] = None, + rule_message: Optional[str] = None, api_name: Optional[str] = None, + field_rule_type: Optional[FieldRuleType.LITERALS] = None, + field_rule_operator: Optional[FieldRuleOperator.LITERALS] = None, + field_rule_value: Optional[str] = None, + field_rule_message: Optional[str] = None, ) -> FieldsetTemplate: """Creating fieldset templates.""" @@ -911,20 +931,31 @@ def create_test_shared_fieldset( api_name=api_name, is_shared=True, ) - if rule_type: - FieldsetTemplateRule.objects.create( + if rule_operator: + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, account=account, api_name=f'{fieldset.api_name}-shared-rule-1', - type=rule_type, + message=rule_message, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + api_name=f'{fieldset.api_name}-group-or-1', + ) + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + api_name=f'{fieldset.api_name}-group-and-1', + operator=rule_operator, value=rule_value, ) - if rule_type == FieldSetRuleType.SUM_EQUAL: + if rule_operator == FieldSetRuleOperator.SUM_EQUAL: field_type = FieldType.NUMBER else: field_type = FieldType.STRING - FieldTemplate.objects.create( + field = FieldTemplate.objects.create( name='Fieldset field', type=field_type, fieldset=fieldset, @@ -932,6 +963,28 @@ def create_test_shared_fieldset( api_name=f'{fieldset.api_name}-shared-field-1', account=account, ) + if field_rule_type: + field_ruleset = FieldTemplateRuleSet.objects.create( + account=account, + field=field, + api_name=f'{fieldset.api_name}-field-ruleset-1', + type=field_rule_type, + message=field_rule_message, + order=1, + ) + field_group_or = FieldTemplateRuleGroupOr.objects.create( + account=account, + field_rule=field_ruleset, + api_name=f'{fieldset.api_name}-field-group-or-1', + ) + FieldTemplateRuleGroupAnd.objects.create( + account=account, + group_or=field_group_or, + api_name=f'{fieldset.api_name}-field-group-and-1', + field=field.api_name, + operator=field_rule_operator or FieldRuleOperator.EQUAL, + value=field_rule_value, + ) return fieldset @@ -945,7 +998,7 @@ def create_test_fieldset_template( order: int = 0, api_name: Optional[str] = None, shared_fieldset: Optional[FieldsetTemplate] = None, - rule_type: Optional[FieldSetRuleType.LITERALS] = None, + rule_operator: Optional[FieldSetRuleOperator.LITERALS] = None, rule_value: Optional[str] = None, ) -> FieldsetTemplate: @@ -954,7 +1007,7 @@ def create_test_fieldset_template( if shared_fieldset is None: shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) @@ -973,14 +1026,26 @@ def create_test_fieldset_template( is_shared=False, shared_fieldset=shared_fieldset, ) - for shared_rule in shared_fieldset.rules.all(): - FieldsetTemplateRule.objects.create( + for shared_ruleset in shared_fieldset.rulesets.all(): + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, account=account, - api_name=f'{fieldset.api_name}-rule-1', - type=shared_rule.type, - value=shared_rule.value, + api_name=f'{fieldset.api_name}-ruleset-1', ) + for shared_group_or in shared_ruleset.groups_or.all(): + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + api_name=f'{fieldset.api_name}-group-or-1', + ) + for shared_group_and in shared_group_or.groups_and.all(): + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + api_name=f'{fieldset.api_name}-group-and-1', + operator=shared_group_and.operator, + value=shared_group_and.value, + ) for shared_field in shared_fieldset.fields.all(): field_template = FieldTemplate.objects.create( @@ -1011,15 +1076,16 @@ def create_test_fieldset( order: int = 0, label_position: LabelPosition.LITERALS = LabelPosition.TOP, layout: FieldSetLayout.LITERALS = FieldSetLayout.VERTICAL, - rule_type: Optional[FieldSetRuleType.LITERALS] = None, + rule_operator: Optional[FieldSetRuleOperator.LITERALS] = None, rule_value: Optional[str] = None, + rule_message: Optional[str] = None, api_name: Optional[str] = None, ) -> FieldSet: """Creating a workflow FieldSet with one TaskField.""" - + account = workflow.account fieldset = FieldSet.objects.create( - account=workflow.account, + account=account, workflow=workflow, kickoff=kickoff, task=task, @@ -1030,15 +1096,29 @@ def create_test_fieldset( layout=layout, api_name=api_name, ) - if rule_type: - FieldSetRule.objects.create( + if rule_operator: + ruleset = FieldSetRuleSet.objects.create( + workflow=workflow, fieldset=fieldset, - account=workflow.account, + account=account, api_name=f'{fieldset.api_name}-rule-1', - type=rule_type, + message=rule_message, + ) + group_or = FieldSetRuleGroupOr.objects.create( + workflow=workflow, + fieldset_rule=ruleset, + account=account, + api_name=f'{fieldset.api_name}-group-or-1', + ) + FieldSetRuleGroupAnd.objects.create( + workflow=workflow, + group_or=group_or, + account=account, + api_name=f'{fieldset.api_name}-group-and-1', + operator=rule_operator, value=rule_value, ) - if rule_type == FieldSetRuleType.SUM_EQUAL: + if rule_operator == FieldSetRuleOperator.SUM_EQUAL: field_type = FieldType.NUMBER field_value = '10' else: diff --git a/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_rule_service.py b/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_rule_service.py index 537f0e7a1..b9eed241a 100644 --- a/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_rule_service.py +++ b/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_rule_service.py @@ -1,24 +1,28 @@ from decimal import Decimal +from types import SimpleNamespace import pytest + from src.authentication.enums import AuthTokenType from src.processes.enums import ( - FieldSetRuleType, + FieldSetRuleOperator, FieldType, ) from src.processes.messages import fieldset as fs_messages +from src.processes.models.templates.fields import FieldTemplate from src.processes.models.templates.fieldset import ( FieldsetTemplate, - FieldsetTemplateRule, + FieldSetTemplateRuleGroupAnd, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleSet, ) -from src.processes.models.templates.fields import FieldTemplate from src.processes.services.exceptions import ( FieldsetTemplateRuleServiceException, FieldsetTemplateRuleSumMaxFieldsNotNumber, FieldsetTemplateRuleSumMaxInvalidValue, ) from src.processes.services.fieldsets.fieldset_rule import ( - FieldsetTemplateRuleService, + FieldsetTemplateRuleSetService, ) from src.processes.tests.fixtures import ( create_test_account, @@ -29,868 +33,3030 @@ pytestmark = pytest.mark.django_db -def test__create_instance__default_params__ok(): +def test__validate_sum__default_params__ok(): """ - Call with default parameters + Default parameters """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - service = FieldsetTemplateRuleService( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + field_1 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset, + name='Field 1', + type=FieldType.NUMBER, + order=1, + ) + ruleset.fields.add(field_1) + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, + instance=ruleset, ) # act - result = service._create_instance( - type=FieldSetRuleType.SUM_EQUAL, - fieldset_id=fieldset.id, - ) + result = service._validate_sum(group_and=group_and) # assert - assert service.instance is not None - assert result is service.instance - assert service.instance.type == FieldSetRuleType.SUM_EQUAL - assert service.instance.value is None - assert service.instance.fieldset_id == fieldset.id - assert service.instance.account_id == account.id - assert service.instance.api_name + assert result == Decimal(100) -def test__create_instance__all_params__ok(): +def test__validate_sum__falsy_value__raise_exception(): """ - Call with all parameters + Falsy value """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - service = FieldsetTemplateRuleService( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value=None, + ) + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, + instance=ruleset, ) - value = '100' - api_name = 'rule-1' - rule_type = FieldSetRuleType.SUM_EQUAL # act - service._create_instance( - type=rule_type, - value=value, - api_name=api_name, - fieldset_id=fieldset.id, - ) + with pytest.raises(FieldsetTemplateRuleSumMaxInvalidValue) as ex: + service._validate_sum(group_and=group_and) # assert - assert service.instance is not None - assert service.instance.type == rule_type - assert service.instance.value == value - assert service.instance.fieldset_id == fieldset.id - assert service.instance.account_id == account.id - assert service.instance.api_name == api_name + assert ex.value.message == fs_messages.MSG_FS_0004 -def test__validate_sum_equal__valid__ok(): +def test__validate_sum__invalid_decimal__raise_exception(): """ - Value from kwargs, valid number, all NUMBER fields → ok + Invalid decimal """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - field = FieldTemplate.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - name='Number field', - type=FieldType.NUMBER, - api_name='num', - order=1, + account=account, + template=template, ) - rule = FieldsetTemplateRule.objects.create( + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100.5', + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='not-a-number', ) - rule.fields.add(field) - service = FieldsetTemplateRuleService( + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, + instance=ruleset, ) # act - result = service._validate_sum_equal() + with pytest.raises(FieldsetTemplateRuleSumMaxInvalidValue) as ex: + service._validate_sum(group_and=group_and) # assert - assert result == Decimal('100.5') + assert ex.value.message == fs_messages.MSG_FS_0004 -def test__validate_sum_equal__empty_value__raise_exception(): +def test__validate_sum__non_number_fields__raise_exception(): """ - Value is empty → raises SumMaxInvalidValue + Non-number fields """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - rule = FieldsetTemplateRule.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + field_1 = FieldTemplate.objects.create( account=account, + template=template, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value=None, + name='Field 1', + type=FieldType.STRING, + order=1, ) - service = FieldsetTemplateRuleService( + ruleset.fields.add(field_1) + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, + instance=ruleset, ) # act - with pytest.raises(FieldsetTemplateRuleSumMaxInvalidValue) as ex: - service._validate_sum_equal() + with pytest.raises(FieldsetTemplateRuleSumMaxFieldsNotNumber) as ex: + service._validate_sum(group_and=group_and) # assert - assert ex.value.message == fs_messages.MSG_FS_0004 + assert ex.value.message == fs_messages.MSG_FS_0003 -def test__validate_sum_equal__non_numeric__raise_exception(): +def test__validate_sum__all_fields_are_number__ok(): """ - Value is non-numeric → raises SumMaxInvalidValue + All fields are NUMBER """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - rule = FieldsetTemplateRule.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='abc', + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='0.3', ) - service = FieldsetTemplateRuleService( + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, + instance=ruleset, ) # act - with pytest.raises(FieldsetTemplateRuleSumMaxInvalidValue) as ex: - service._validate_sum_equal() + result = service._validate_sum(group_and=group_and) # assert - assert ex.value.message == fs_messages.MSG_FS_0004 + assert result == Decimal('0.3') -@pytest.mark.parametrize( - 'field_type', - ( - FieldType.STRING, - FieldType.TEXT, - FieldType.RADIO, - FieldType.CHECKBOX, - FieldType.DATE, - FieldType.URL, - FieldType.DROPDOWN, - FieldType.FILE, - FieldType.USER, - ), -) -def test__validate_sum_equal__non_number_type__raise_exception(field_type): +def test__get_valid_fields__default_params__ok(): """ - Non-NUMBER field exists → raises SumMaxFieldsNotNumber + Default parameters """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - field = FieldTemplate.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - name='String field', - type=field_type, - api_name='str_field', - order=1, + account=account, + template=template, ) - rule = FieldsetTemplateRule.objects.create( + field_1 = FieldTemplate.objects.create( account=account, + template=template, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100', + name='Field 1', + type=FieldType.NUMBER, + order=1, + api_name='field-1', ) - rule.fields.add(field) - service = FieldsetTemplateRuleService( + fields_api_names = [field_1.api_name] + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, + instance=ruleset, ) # act - with pytest.raises(FieldsetTemplateRuleSumMaxFieldsNotNumber) as ex: - service._validate_sum_equal() + result = service._get_valid_fields(fields_api_names=fields_api_names) # assert - assert ex.value.message == fs_messages.MSG_FS_0003 + assert len(result) == 1 + assert result[0] == field_1 -def test__validate__call_method_by_type__ok(mocker): +def test__get_valid_fields__type_kwarg_provided__raise_exception(): + + """ + type kwarg provided + """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - rule = FieldsetTemplateRule.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100', + account=account, + template=template, ) - service = FieldsetTemplateRuleService( + missing_api_name = 'missing-field-1' + rule_type = 'custom-rule' + fields_api_names = [missing_api_name] + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, - ) - validate_sum_equal_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService._validate_sum_equal', + instance=ruleset, ) - kwargs = {'type': FieldSetRuleType.SUM_EQUAL} # act - service._validate(**kwargs) + with pytest.raises(FieldsetTemplateRuleServiceException) as ex: + service._get_valid_fields( + fields_api_names=fields_api_names, + type=rule_type, + ) # assert - validate_sum_equal_mock.assert_called_once_with(**kwargs) + assert ex.value.message == fs_messages.MSG_FS_0005( + rule=rule_type, + field=missing_api_name, + ) -def test_get_valid_fields__all_found__ok(): +def test__get_valid_fields__type_kwarg_omitted_or_falsy__raise_exception(): """ - All fields found → returns list + type kwarg omitted or falsy """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - field_1_api_name = 'field_1' - field1 = FieldTemplate.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - name='Field 1', - type=FieldType.STRING, - api_name=field_1_api_name, - order=1, - ) - field_2_api_name = 'field_2' - field2 = FieldTemplate.objects.create( account=account, - fieldset=fieldset, - name='Field 2', - type=FieldType.NUMBER, - api_name=field_2_api_name, - order=2, + template=template, ) - service = FieldsetTemplateRuleService( + missing_api_name = 'missing-field-1' + fields_api_names = [missing_api_name] + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - ) - service.instance = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + instance=ruleset, ) # act - result = service._get_valid_fields([field_1_api_name, field_2_api_name]) + with pytest.raises(FieldsetTemplateRuleServiceException) as ex: + service._get_valid_fields( + fields_api_names=fields_api_names, + type=None, + ) # assert - assert len(result) == 2 - assert field1 in result - assert field2 in result + assert ex.value.message == fs_messages.MSG_FS_0005( + field=missing_api_name, + ) -def test_get_valid_fields__type_from_kwargs__ok(): +def test__get_valid_fields__all_api_names_found__ok(): """ - rule_type from kwargs + All api_names found """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - field_api_name = 'field_1' - field = FieldTemplate.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + field_1 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset, + name='Field 1', + type=FieldType.NUMBER, + order=2, + api_name='field-1', + ) + field_2 = FieldTemplate.objects.create( account=account, + template=template, fieldset=fieldset, - name='Number field', + name='Field 2', type=FieldType.NUMBER, - api_name=field_api_name, order=1, + api_name='field-2', ) - service = FieldsetTemplateRuleService( + fields_api_names = [field_1.api_name, field_2.api_name] + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - ) - service.instance = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + instance=ruleset, ) # act - result = service._get_valid_fields( - fields_api_names=[field_api_name], - type=FieldSetRuleType.SUM_EQUAL, - ) + result = service._get_valid_fields(fields_api_names=fields_api_names) # assert - assert result == [field] + assert len(result) == 2 + assert result[0] == field_1 + assert result[1] == field_2 -def test_get_valid_fields__type_from_instance__ok(): +def test__get_valid_fields__missing_api_name__raise_exception(): """ - rule_type from instance + Missing api_name """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) - fieldset = FieldsetTemplate.objects.create( + template = create_test_template(user=user) + fieldset_1 = FieldsetTemplate.objects.create( + account=account, template=template, + name='Fieldset 1', + ) + fieldset_2 = FieldsetTemplate.objects.create( account=account, - name='Fieldset', + template=template, + name='Fieldset 2', ) - field_api_name = 'field_1' - field = FieldTemplate.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset_1, account=account, - fieldset=fieldset, - name='Number field', + template=template, + ) + field_1 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset_2, + name='Field 1', type=FieldType.NUMBER, - api_name=field_api_name, order=1, + api_name='field-1', ) - service = FieldsetTemplateRuleService( + fields_api_names = [field_1.api_name] + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - ) - service.instance = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + instance=ruleset, ) # act - result = service._get_valid_fields( - fields_api_names=[field_api_name], - ) + with pytest.raises(FieldsetTemplateRuleServiceException) as ex: + service._get_valid_fields(fields_api_names=fields_api_names) # assert - assert result == [field] + assert ex.value.message == fs_messages.MSG_FS_0005( + field=field_1.api_name, + ) -def test_get_valid_fields__one_failed__raise_exception(): +def test__get_valid_fields__empty_fields_api_names__ok(): """ - failed_api_names has 1 element → raises exception + Empty fields_api_names """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - FieldTemplate.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - name='Number field', - type=FieldType.NUMBER, - api_name='num', - order=1, + account=account, + template=template, ) - service = FieldsetTemplateRuleService( + fields_api_names = [] + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - ) - service.instance = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + instance=ruleset, ) # act - with pytest.raises(FieldsetTemplateRuleServiceException) as ex: - service._get_valid_fields(['missing']) + result = service._get_valid_fields(fields_api_names=fields_api_names) # assert - assert ex.value.message == fs_messages.MSG_FS_0005( - rule=FieldSetRuleType.SUM_EQUAL, - field='missing', - ) + assert result == [] -def test_get_valid_fields__two_failed__raise_exception(): +def test__create_instance__default_params__ok(): """ - failed_api_names has 2 elements → raises exception + Default parameters """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - service = FieldsetTemplateRuleService( + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, ) - service.instance = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - ) # act - with pytest.raises(FieldsetTemplateRuleServiceException) as ex: - service._get_valid_fields(['missing1', 'missing2']) + result = service._create_instance( + fieldset_id=fieldset.id, + ) # assert - assert ex.value.message in { - fs_messages.MSG_FS_0005( - rule=FieldSetRuleType.SUM_EQUAL, - field='missing1', - ), fs_messages.MSG_FS_0005( - rule=FieldSetRuleType.SUM_EQUAL, - field='missing2', - ), - } + assert result == service.instance + assert result.fieldset_id == fieldset.id + assert result.message is None + assert result.order == 0 + assert result.template_id == template.id + assert result.account_id == account.id + assert result.api_name.startswith('fieldset-ruleset') -def test_set_fields__fields_provided__set_fields(mocker): +def test__create_instance__resolve_template_id_from_fieldset__ok(): """ - Non-empty list → fields are set + Resolve template_id from fieldset """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - field_api_name = 'num' - field = FieldTemplate.objects.create( - account=account, - fieldset=fieldset, - name='Number field', - type=FieldType.NUMBER, - api_name=field_api_name, - order=1, - ) - service = FieldsetTemplateRuleService( + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, ) - rule = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - ) - service.instance = rule - get_valid_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._get_valid_fields', - return_value=[field], - ) # act - service._set_fields([field_api_name]) + result = service._create_instance( + fieldset_id=fieldset.id, + template_id=None, + ) # assert - get_valid_fields_mock.assert_called_once_with(['num']) - assert list(rule.fields.all()) == [field] + assert result.template_id == fieldset.template_id -def test_set_fields__fields_not_provided__clear_fields(mocker): +def test__create_instance__explicit_template_id__ok(): """ - Empty list → fields are cleared + Explicit template_id """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template_1 = create_test_template(user=user) + template_2 = create_test_template( + user=user, + name='Test workflow 2', + ) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template_1, name='Fieldset', ) - field = FieldTemplate.objects.create( - account=account, - fieldset=fieldset, - name='Number field', - type=FieldType.NUMBER, - api_name='num', - order=1, - ) - rule = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - ) - rule.fields.add(field) - service = FieldsetTemplateRuleService( + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, ) - service.instance = rule - get_valid_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._get_valid_fields', - ) # act - service._set_fields([]) + result = service._create_instance( + fieldset_id=fieldset.id, + template_id=template_2.id, + ) # assert - get_valid_fields_mock.assert_not_called() - assert rule.fields.count() == 0 + assert result.template_id == template_2.id + assert result.fieldset_id == fieldset.id -def test_create_related__fields_provided__ok(mocker): +def test__create_instance__api_name_provided__ok(): """ - fields present in kwargs + api_name provided """ # arrange account = create_test_account() user = create_test_owner(account=account) - service = FieldsetTemplateRuleService( + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, ) - set_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._set_fields', - ) - fields = ['num'] + api_name = 'ruleset-custom-1' # act - service._create_related(fields=fields) + result = service._create_instance( + fieldset_id=fieldset.id, + api_name=api_name, + ) # assert - set_fields_mock.assert_called_once_with(fields) + assert result.api_name == api_name -def test_create_related__fields_provided_empty_list__ok(mocker): +def test__create_instance__api_name_omitted__ok(): """ - fields not in kwargs → no-op + api_name omitted """ # arrange account = create_test_account() user = create_test_owner(account=account) - service = FieldsetTemplateRuleService( + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, ) - set_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._set_fields', - ) # act - service._create_related(fields=[]) + result = service._create_instance( + fieldset_id=fieldset.id, + api_name='', + ) # assert - set_fields_mock.assert_called_once_with([]) + assert result.api_name.startswith('fieldset-ruleset') -def test_create_related__fields_not_provided__skip(mocker): +def test__validate__default_params__ok(mocker): """ - fields not in kwargs → no-op + Default parameters """ # arrange account = create_test_account() user = create_test_owner(account=account) - service = FieldsetTemplateRuleService( + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_sum_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate_sum', + ) + + # act + result = service._validate(group_and=group_and) + + # assert + assert result is None + validate_sum_mock.assert_called_once_with(group_and=group_and) + + +def test__validate__type_is_not_validator__ok(mocker): + + """ + Type is not VALIDATOR + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + type='show', + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_sum_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate_sum', + ) + + # act + result = service._validate(group_and=group_and) + + # assert + assert result is None + validate_sum_mock.assert_not_called() + + +def test__validate__operator_not_a_sum_operator__ok(mocker): + + """ + Operator not a sum operator + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator='equal', + value='100', + ) + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_sum_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate_sum', + ) + + # act + result = service._validate(group_and=group_and) + + # assert + assert result is None + validate_sum_mock.assert_not_called() + + +def test__set_fields__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + field_1 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset, + name='Field 1', + type=FieldType.NUMBER, + order=1, + api_name='field-1', + ) + fields_api_names = [field_1.api_name] + valid_fields = [field_1] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + get_valid_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._get_valid_fields', + return_value=valid_fields, + ) + + # act + service._set_fields(fields_api_names=fields_api_names) + + # assert + assert service.instance.fields.count() == 1 + assert service.instance.fields.all()[0] == field_1 + get_valid_fields_mock.assert_called_once_with( + fields_api_names=fields_api_names, + ) + + +def test__set_fields__non_empty_fields_api_names__ok(mocker): + + """ + Non-empty fields_api_names + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + field_1 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset, + name='Field 1', + type=FieldType.NUMBER, + order=2, + api_name='field-1', + ) + field_2 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset, + name='Field 2', + type=FieldType.NUMBER, + order=1, + api_name='field-2', + ) + fields_api_names = [field_1.api_name, field_2.api_name] + valid_fields = [field_1, field_2] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + get_valid_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._get_valid_fields', + return_value=valid_fields, + ) + + # act + service._set_fields(fields_api_names=fields_api_names) + + # assert + assert service.instance.fields.count() == 2 + assert service.instance.fields.all()[0] == field_1 + assert service.instance.fields.all()[1] == field_2 + get_valid_fields_mock.assert_called_once_with( + fields_api_names=fields_api_names, + ) + + +def test__set_fields__empty_fields_api_names__ok(mocker): + + """ + Empty fields_api_names + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + field_1 = FieldTemplate.objects.create( + account=account, + template=template, + fieldset=fieldset, + name='Field 1', + type=FieldType.NUMBER, + order=1, + api_name='field-1', + ) + ruleset.fields.add(field_1) + fields_api_names = [] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + get_valid_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._get_valid_fields', + ) + + # act + service._set_fields(fields_api_names=fields_api_names) + + # assert + assert service.instance.fields.count() == 0 + get_valid_fields_mock.assert_not_called() + + +def test__create_group_and__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and_data = { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._create_group_and( + group_or=group_or, + group_and_data=group_and_data, + ) + + # assert + assert result.group_or_id == group_or.id + assert result.account_id == account.id + assert result.template_id == template.id + assert result.operator == FieldSetRuleOperator.SUM_EQUAL + assert result.value is None + assert result.api_name.startswith('fieldset-rule-group-and') + validate_mock.assert_called_once_with(group_and=result) + + +def test__create_group_and__api_name_provided__ok(mocker): + + """ + api_name provided + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + api_name = 'group-and-custom-1' + group_and_data = { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'api_name': api_name, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._create_group_and( + group_or=group_or, + group_and_data=group_and_data, + ) + + # assert + assert result.api_name == api_name + validate_mock.assert_called_once_with(group_and=result) + + +def test__create_group_and__api_name_omitted__ok(mocker): + + """ + api_name omitted + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and_data = { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'api_name': '', + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._create_group_and( + group_or=group_or, + group_and_data=group_and_data, + ) + + # assert + assert result.api_name.startswith('fieldset-rule-group-and') + validate_mock.assert_called_once_with(group_and=result) + + +def test__create_group_and__value_provided__ok(mocker): + + """ + value provided + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + value = '200' + group_and_data = { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': value, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._create_group_and( + group_or=group_or, + group_and_data=group_and_data, + ) + + # assert + assert result.value == value + validate_mock.assert_called_once_with(group_and=result) + + +def test__update_group_and__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + group_and_data = {} + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._update_group_and( + group_and=group_and, + group_and_data=group_and_data, + ) + + # assert + group_and.refresh_from_db() + assert result == group_and + assert group_and.operator == FieldSetRuleOperator.SUM_EQUAL + assert group_and.value == '100' + validate_mock.assert_called_once_with(group_and=group_and) + + +def test__update_group_and__operator_in_payload__ok(mocker): + + """ + operator in payload + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + operator = FieldSetRuleOperator.SUM_GREATER_THAN + group_and_data = { + 'operator': operator, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._update_group_and( + group_and=group_and, + group_and_data=group_and_data, + ) + + # assert + group_and.refresh_from_db() + assert result == group_and + assert group_and.operator == operator + assert group_and.value == '100' + validate_mock.assert_called_once_with(group_and=group_and) + + +def test__update_group_and__value_in_payload__ok(mocker): + + """ + value in payload + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + value = '200' + group_and_data = { + 'value': value, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._update_group_and( + group_and=group_and, + group_and_data=group_and_data, + ) + + # assert + group_and.refresh_from_db() + assert result == group_and + assert group_and.operator == FieldSetRuleOperator.SUM_EQUAL + assert group_and.value == value + validate_mock.assert_called_once_with(group_and=group_and) + + +def test__update_group_and__operator_and_value_in_payload__ok(mocker): + + """ + operator and value in payload + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + operator = FieldSetRuleOperator.SUM_LESS_THAN + value = '50' + group_and_data = { + 'operator': operator, + 'value': value, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._update_group_and( + group_and=group_and, + group_and_data=group_and_data, + ) + + # assert + group_and.refresh_from_db() + assert result == group_and + assert group_and.operator == operator + assert group_and.value == value + validate_mock.assert_called_once_with(group_and=group_and) + + +def test__update_group_and__no_operator_or_value__ok(mocker): + + """ + No operator or value + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + group_and_data = { + 'api_name': 'group-and-1', + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + validate_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._validate', + ) + + # act + result = service._update_group_and( + group_and=group_and, + group_and_data=group_and_data, + ) + + # assert + group_and.refresh_from_db() + assert result == group_and + assert group_and.operator == FieldSetRuleOperator.SUM_EQUAL + assert group_and.value == '100' + validate_mock.assert_called_once_with(group_and=group_and) + + +def test__set_groups_and__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and_1 = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + groups_and_data = [] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_and', + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + service._set_groups_and( + group_or=group_or, + groups_and_data=groups_and_data, + ) + + # assert + assert not group_or.groups_and.filter(id=group_and_1.id).exists() + update_group_and_mock.assert_not_called() + create_group_and_mock.assert_not_called() + + +def test__set_groups_and__matching_api_name__ok(mocker): + + """ + Matching api_name + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and_1 = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + group_and_data_1 = { + 'api_name': group_and_1.api_name, + 'operator': FieldSetRuleOperator.SUM_EQUAL, + } + groups_and_data = [group_and_data_1] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_and', + return_value=group_and_1, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + service._set_groups_and( + group_or=group_or, + groups_and_data=groups_and_data, + ) + + # assert + assert group_or.groups_and.filter(id=group_and_1.id).exists() + update_group_and_mock.assert_called_once_with( + group_and=group_and_1, + group_and_data=group_and_data_1, + ) + create_group_and_mock.assert_not_called() + + +def test__set_groups_and__unknown_or_missing_api_name__ok(mocker): + + """ + Unknown or missing api_name + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and_1 = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + group_and_data_1 = { + 'operator': FieldSetRuleOperator.SUM_GREATER_THAN, + 'value': '50', + } + groups_and_data = [group_and_data_1] + created_group_and = SimpleNamespace(api_name='group-and-2') + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_and', + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + return_value=created_group_and, + ) + + # act + service._set_groups_and( + group_or=group_or, + groups_and_data=groups_and_data, + ) + + # assert + assert not group_or.groups_and.filter(id=group_and_1.id).exists() + create_group_and_mock.assert_called_once_with( + group_or=group_or, + group_and_data=group_and_data_1, + ) + update_group_and_mock.assert_not_called() + + +def test__set_groups_and__mixed_payload__ok(mocker): + + """ + Mixed payload + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + ) + group_and_1 = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + group_and_2 = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + api_name='group-and-2', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='200', + ) + group_and_data_1 = { + 'api_name': group_and_1.api_name, + 'operator': FieldSetRuleOperator.SUM_EQUAL, + } + group_and_data_2 = { + 'operator': FieldSetRuleOperator.SUM_GREATER_THAN, + 'value': '50', + } + groups_and_data = [group_and_data_1, group_and_data_2] + created_group_and = SimpleNamespace(api_name='group-and-3') + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_and', + return_value=group_and_1, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + return_value=created_group_and, + ) + + # act + service._set_groups_and( + group_or=group_or, + groups_and_data=groups_and_data, + ) + + # assert + assert group_or.groups_and.filter(id=group_and_1.id).exists() + assert not group_or.groups_and.filter(id=group_and_2.id).exists() + update_group_and_mock.assert_called_once_with( + group_and=group_and_1, + group_and_data=group_and_data_1, + ) + create_group_and_mock.assert_called_once_with( + group_or=group_or, + group_and_data=group_and_data_2, + ) + + +def test__create_group_or__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_data = {} + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + result = service._create_group_or(group_or_data=group_or_data) + + # assert + assert result.fieldset_rule_id == ruleset.id + assert result.account_id == account.id + assert result.template_id == template.id + assert result.api_name.startswith('fieldset-rule-group-or') + create_group_and_mock.assert_not_called() + + +def test__create_group_or__api_name_provided__ok(mocker): + + """ + api_name provided + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + api_name = 'group-or-custom-1' + group_or_data = { + 'api_name': api_name, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + result = service._create_group_or(group_or_data=group_or_data) + + # assert + assert result.api_name == api_name + create_group_and_mock.assert_not_called() + + +def test__create_group_or__api_name_omitted__ok(mocker): + + """ + api_name omitted + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_data = { + 'api_name': '', + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + result = service._create_group_or(group_or_data=group_or_data) + + # assert + assert result.api_name.startswith('fieldset-rule-group-or') + create_group_and_mock.assert_not_called() + + +def test__create_group_or__groups_and_omitted__ok(mocker): + + """ + groups_and omitted + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_data = { + 'api_name': 'group-or-1', + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + result = service._create_group_or(group_or_data=group_or_data) + + # assert + assert result.api_name == 'group-or-1' + assert result.groups_and.count() == 0 + create_group_and_mock.assert_not_called() + + +def test__create_group_or__groups_and_is_not_none__ok(mocker): + + """ + groups_and is not None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_and_data_1 = { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + } + group_or_data = { + 'groups_and': [group_and_data_1], + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + create_group_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_and', + ) + + # act + result = service._create_group_or(group_or_data=group_or_data) + + # assert + assert result.fieldset_rule_id == ruleset.id + create_group_and_mock.assert_called_once_with( + group_or=result, + group_and_data=group_and_data_1, + ) + + +def test__update_group_or__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + group_or_data = {} + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_groups_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_and', + ) + + # act + result = service._update_group_or( + group_or=group_or, + group_or_data=group_or_data, + ) + + # assert + assert result == group_or + assert result.api_name == 'group-or-1' + set_groups_and_mock.assert_not_called() + + +def test__update_group_or__groups_and_omitted__ok(mocker): + + """ + groups_and omitted + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + group_and_1 = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + template=template, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='100', + ) + group_or_data = { + 'groups_and': None, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_groups_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_and', + ) + + # act + result = service._update_group_or( + group_or=group_or, + group_or_data=group_or_data, + ) + + # assert + assert result == group_or + assert group_or.groups_and.filter(id=group_and_1.id).exists() + set_groups_and_mock.assert_not_called() + + +def test__update_group_or__groups_and_is_not_none__ok(mocker): + + """ + groups_and is not None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + groups_and_data = [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ] + group_or_data = { + 'groups_and': groups_and_data, + } + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_groups_and_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_and', + ) + + # act + result = service._update_group_or( + group_or=group_or, + group_or_data=group_or_data, + ) + + # assert + assert result == group_or + set_groups_and_mock.assert_called_once_with( + group_or=group_or, + groups_and_data=groups_and_data, + ) + + +def test__set_groups_or__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_1 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + groups_or_data = [] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_or', + ) + create_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_or', + ) + + # act + service._set_groups_or(groups_or_data=groups_or_data) + + # assert + assert not ruleset.groups_or.filter(id=group_or_1.id).exists() + update_group_or_mock.assert_not_called() + create_group_or_mock.assert_not_called() + + +def test__set_groups_or__matching_api_name__ok(mocker): + + """ + Matching api_name + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_1 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + group_or_data_1 = { + 'api_name': group_or_1.api_name, + } + groups_or_data = [group_or_data_1] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_or', + return_value=group_or_1, + ) + create_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_or', + ) + + # act + service._set_groups_or(groups_or_data=groups_or_data) + + # assert + assert ruleset.groups_or.filter(id=group_or_1.id).exists() + update_group_or_mock.assert_called_once_with( + group_or=group_or_1, + group_or_data=group_or_data_1, + ) + create_group_or_mock.assert_not_called() + + +def test__set_groups_or__unknown_or_missing_api_name__ok(mocker): + + """ + Unknown or missing api_name + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_1 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + group_or_data_1 = { + 'api_name': 'unknown-group-or-1', + } + groups_or_data = [group_or_data_1] + created_group_or = SimpleNamespace(api_name='unknown-group-or-1') + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_or', + ) + create_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_or', + return_value=created_group_or, + ) + + # act + service._set_groups_or(groups_or_data=groups_or_data) + + # assert + assert not ruleset.groups_or.filter(id=group_or_1.id).exists() + create_group_or_mock.assert_called_once_with( + group_or_data=group_or_data_1, + ) + update_group_or_mock.assert_not_called() + + +def test__set_groups_or__mixed_payload__ok(mocker): + + """ + Mixed payload + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + group_or_1 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-1', + ) + group_or_2 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + template=template, + api_name='group-or-2', + ) + group_or_data_1 = { + 'api_name': group_or_1.api_name, + } + group_or_data_2 = { + 'api_name': 'group-or-3', + } + groups_or_data = [group_or_data_1, group_or_data_2] + created_group_or = SimpleNamespace(api_name='group-or-3') + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + update_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._update_group_or', + return_value=group_or_1, + ) + create_group_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._create_group_or', + return_value=created_group_or, + ) + + # act + service._set_groups_or(groups_or_data=groups_or_data) + + # assert + assert ruleset.groups_or.filter(id=group_or_1.id).exists() + assert not ruleset.groups_or.filter(id=group_or_2.id).exists() + update_group_or_mock.assert_called_once_with( + group_or=group_or_1, + group_or_data=group_or_data_1, + ) + create_group_or_mock.assert_called_once_with( + group_or_data=group_or_data_2, + ) + + +def test__create_related__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service._create_related() + + # assert + assert result is None + set_fields_mock.assert_not_called() + set_groups_or_mock.assert_not_called() + + +def test__create_related__fields_is_none__ok(mocker): + + """ + fields is None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, + instance=ruleset, ) set_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._set_fields', + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', ) # act - service._create_related() + result = service._create_related(fields=None) # assert + assert result is None set_fields_mock.assert_not_called() + set_groups_or_mock.assert_not_called() + + +def test__create_related__fields_is_not_none__ok(mocker): + + """ + fields is not None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + fields = ['field-1'] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service._create_related(fields=fields) + + # assert + assert result is None + set_fields_mock.assert_called_once_with(fields_api_names=fields) + set_groups_or_mock.assert_not_called() -def test_create__valid_data__ok(mocker): +def test__create_related__groups_or_is_none__ok(mocker): """ - Call with valid data → returns instance + groups_or is None """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, template=template, + ) + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service._create_related(groups_or=None) + + # assert + assert result is None + set_fields_mock.assert_not_called() + set_groups_or_mock.assert_not_called() + + +def test__create_related__groups_or_is_not_none__ok(mocker): + + """ + groups_or is not None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( account=account, + template=template, name='Fieldset', ) - rule = FieldsetTemplateRule.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + ) + groups_or = [ + { + 'api_name': 'group-or-1', + }, + ] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service._create_related(groups_or=groups_or) + + # assert + assert result is None + set_groups_or_mock.assert_called_once_with(groups_or_data=groups_or) + set_fields_mock.assert_not_called() + + +def test_create__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100', + account=account, + template=template, ) - service = FieldsetTemplateRuleService( + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, + instance=ruleset, ) - service.instance = rule create_instance_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService._create_instance', + 'FieldsetTemplateRuleSetService._create_instance', ) create_related_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService._create_related', + 'FieldsetTemplateRuleSetService._create_related', ) create_actions_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService._create_actions', + 'FieldsetTemplateRuleSetService._create_actions', ) - validate_mock = mocker.patch( + + # act + result = service.create() + + # assert + assert result == ruleset + create_instance_mock.assert_called_once_with() + create_related_mock.assert_called_once_with() + create_actions_mock.assert_called_once_with() + + +def test_partial_update__default_params__ok(mocker): + + """ + Default parameters + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + message=None, + order=0, + ) + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService._validate', + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', ) - - kwargs = { - 'fields': ['num'], - 'value': '100', - 'type': FieldSetRuleType.SUM_EQUAL, - 'fieldset_id': fieldset.id, - } # act - result = service.create(**kwargs) + result = service.partial_update() # assert - assert result == rule - create_instance_mock.assert_called_once_with(**kwargs) - create_related_mock.assert_called_once_with(**kwargs) - create_actions_mock.assert_called_once_with(**kwargs) - validate_mock.assert_called_once_with(**kwargs) + ruleset.refresh_from_db() + assert result == ruleset + assert ruleset.message is None + assert ruleset.order == 0 + set_fields_mock.assert_not_called() + set_groups_or_mock.assert_not_called() -def test_partial_update__with_fields__ok(mocker): +def test_partial_update__scalar_fields_updated__ok(mocker): """ - fields in update_kwargs → branch taken + Scalar fields updated """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( - template=template, account=account, + template=template, name='Fieldset', ) - rule = FieldsetTemplateRule.objects.create( - account=account, + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100', + account=account, + template=template, + message=None, + order=0, ) - service = FieldsetTemplateRuleService( + message = 'Custom message' + order = 5 + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, + instance=ruleset, ) set_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._set_fields', + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', ) - validate_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._validate', + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', ) - value = '200' - fields = ['num'] # act - result = service.partial_update(value=value, fields=fields) + result = service.partial_update( + message=message, + order=order, + ) # assert - set_fields_mock.assert_called_once_with(fields) - validate_mock.assert_called_once_with(value=value) - assert result == rule - rule.refresh_from_db() - assert rule.value == value + ruleset.refresh_from_db() + assert result == ruleset + assert ruleset.message == message + assert ruleset.order == order + set_fields_mock.assert_not_called() + set_groups_or_mock.assert_not_called() -def test_partial_update__without_fields__ok(mocker): +def test_partial_update__fields_is_none__ok(mocker): """ - fields not in update_kwargs → branch skipped + fields is None """ # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) + template = create_test_template(user=user) fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, template=template, + message=None, + ) + message = 'Updated message' + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service.partial_update( + message=message, + fields=None, + ) + + # assert + ruleset.refresh_from_db() + assert result == ruleset + assert ruleset.message == message + set_fields_mock.assert_not_called() + set_groups_or_mock.assert_not_called() + + +def test_partial_update__fields_is_not_none__ok(mocker): + + """ + fields is not None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( account=account, + template=template, name='Fieldset', ) - rule = FieldsetTemplateRule.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + message=None, + ) + message = 'Updated message' + fields = ['field-1'] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', + ) + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service.partial_update( + message=message, + fields=fields, + ) + + # assert + ruleset.refresh_from_db() + assert result == ruleset + assert ruleset.message == message + set_fields_mock.assert_called_once_with(fields_api_names=fields) + set_groups_or_mock.assert_not_called() + + +def test_partial_update__groups_or_is_none__ok(mocker): + + """ + groups_or is None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100', + account=account, + template=template, + order=0, ) - service = FieldsetTemplateRuleService( + order = 3 + service = FieldsetTemplateRuleSetService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, - instance=rule, + instance=ruleset, ) set_fields_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._set_fields', + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', ) - validate_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.FieldsetTemplateRuleService' - '._validate', + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', + ) + + # act + result = service.partial_update( + order=order, + groups_or=None, + ) + + # assert + ruleset.refresh_from_db() + assert result == ruleset + assert ruleset.order == order + set_groups_or_mock.assert_not_called() + set_fields_mock.assert_not_called() + + +def test_partial_update__groups_or_is_not_none__ok(mocker): + + """ + groups_or is not None + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + template = create_test_template(user=user) + fieldset = FieldsetTemplate.objects.create( + account=account, + template=template, + name='Fieldset', + ) + ruleset = FieldSetTemplateRuleSet.objects.create( + fieldset=fieldset, + account=account, + template=template, + order=0, + ) + order = 3 + groups_or = [ + { + 'api_name': 'group-or-1', + }, + ] + service = FieldsetTemplateRuleSetService( + user=user, + is_superuser=False, + auth_type=AuthTokenType.USER, + instance=ruleset, + ) + set_fields_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_fields', ) - super_partial_mock = mocker.patch( - 'src.processes.services.fieldsets' - '.fieldset_rule.BaseModelService' - '.partial_update', - return_value=rule, + set_groups_or_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset_rule.' + 'FieldsetTemplateRuleSetService._set_groups_or', ) - value = '200' # act - result = service.partial_update(value=value) + result = service.partial_update( + order=order, + groups_or=groups_or, + ) # assert - super_partial_mock.assert_called_once_with(value=value, force_save=True) + ruleset.refresh_from_db() + assert result == ruleset + assert ruleset.order == order + set_groups_or_mock.assert_called_once_with(groups_or_data=groups_or) set_fields_mock.assert_not_called() - validate_mock.assert_called_once_with(value=value) - assert result == rule diff --git a/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_service.py b/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_service.py index 4e0bcd1ed..9bb4120df 100644 --- a/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_service.py +++ b/backend/src/processes/tests/test_services/test_fieldsets/test_fieldset_template_service.py @@ -2,14 +2,16 @@ from src.authentication.enums import AuthTokenType from src.processes.enums import ( FieldSetLayout, - FieldSetRuleType, + FieldSetRuleOperator, FieldType, LabelPosition, ) from src.processes.messages import fieldset as fs_messages from src.processes.models.templates.fieldset import ( FieldsetTemplate, - FieldsetTemplateRule, + FieldSetTemplateRuleSet, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleGroupAnd, ) from src.processes.models.templates.fields import ( FieldTemplate, @@ -26,9 +28,6 @@ from src.processes.services.fieldsets.fieldset import ( FieldSetTemplateService, ) -from src.processes.services.fieldsets.fieldset_rule import ( - FieldsetTemplateRuleService, -) from src.processes.tests.fixtures import ( create_test_account, create_test_owner, @@ -327,10 +326,10 @@ def test__create_fields__with_data__ok(mocker): ) -def test_create_rules__with_data__ok(mocker): +def test_create_rules__with_data__ok(): """ - Call with rules data + Call with rulesets data """ # arrange @@ -348,35 +347,30 @@ def test_create_rules__with_data__ok(mocker): auth_type=AuthTokenType.USER, instance=fieldset, ) - rules_data = [ - {'type': FieldSetRuleType.SUM_EQUAL, 'value': '100'}, + rulesets_data = [ + { + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], + }, ] - # mock - fieldset_template_rule_service_init_mock = mocker.patch.object( - FieldsetTemplateRuleService, - attribute='__init__', - return_value=None, - ) - fieldset_template_rule_service_create_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService.create', - ) - # act - service.create_rules(rules_data=rules_data) + service.create_rulesets(rulesets_data=rulesets_data) # assert - fieldset_template_rule_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - ) - fieldset_template_rule_service_create_mock.assert_called_once_with( - fieldset_id=fieldset.id, - type=FieldSetRuleType.SUM_EQUAL, - value='100', - ) + assert fieldset.rulesets.count() == 1 + ruleset = fieldset.rulesets.first() + group_and = ruleset.groups_or.first().groups_and.first() + assert group_and.operator == FieldSetRuleOperator.SUM_EQUAL + assert group_and.value == '100' def test__create_related__default_params__ok(mocker): @@ -397,7 +391,7 @@ def test__create_related__default_params__ok(mocker): # mock create_rules_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.create_rules', + 'FieldSetTemplateService.create_rulesets', ) create_fields_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' @@ -412,10 +406,10 @@ def test__create_related__default_params__ok(mocker): create_fields_mock.assert_not_called() -def test__create_related__rules_provided__ok(mocker): +def test__create_related__rulesets_provided__ok(mocker): """ - Rules provided + Rulesets provided """ # arrange @@ -426,12 +420,25 @@ def test__create_related__rules_provided__ok(mocker): is_superuser=False, auth_type=AuthTokenType.USER, ) - rules = [{'type': FieldSetRuleType.SUM_EQUAL, 'value': '100'}] + rulesets = [ + { + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], + }, + ] # mock create_rules_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.create_rules', + 'FieldSetTemplateService.create_rulesets', ) create_fields_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' @@ -439,10 +446,10 @@ def test__create_related__rules_provided__ok(mocker): ) # act - service._create_related(rules=rules) + service._create_related(rulesets=rulesets) # assert - create_rules_mock.assert_called_once_with(rules_data=rules) + create_rules_mock.assert_called_once_with(rulesets_data=rulesets) create_fields_mock.assert_not_called() @@ -465,7 +472,7 @@ def test__create_related__fields_provided__ok(mocker): # mock create_rules_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.create_rules', + 'FieldSetTemplateService.create_rulesets', ) create_fields_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' @@ -483,7 +490,7 @@ def test__create_related__fields_provided__ok(mocker): def test__create_related__both_provided__ok(mocker): """ - Both rules and fields provided + Both rulesets and fields provided """ # arrange @@ -494,13 +501,26 @@ def test__create_related__both_provided__ok(mocker): is_superuser=False, auth_type=AuthTokenType.USER, ) - rules = [{'type': FieldSetRuleType.SUM_EQUAL, 'value': '100'}] + rulesets = [ + { + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], + }, + ] fields = [{'name': 'Field 1', 'type': 'string', 'order': 1}] # mock create_rules_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.create_rules', + 'FieldSetTemplateService.create_rulesets', ) create_fields_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.' @@ -508,10 +528,10 @@ def test__create_related__both_provided__ok(mocker): ) # act - service._create_related(rules=rules, fields=fields) + service._create_related(rulesets=rulesets, fields=fields) # assert - create_rules_mock.assert_called_once_with(rules_data=rules) + create_rules_mock.assert_called_once_with(rulesets_data=rulesets) create_fields_mock.assert_called_once_with(fields_data=fields) @@ -706,10 +726,10 @@ def test__update_fields__orphan_fields__deleted(mocker): assert FieldTemplate.objects.filter(id=field_1.id).exists() -def test__validate_rules__with_rules__ok(mocker): +def test_update_rules__existing_rule__ok(mocker): """ - Call with rules + Update existing ruleset """ # arrange @@ -721,62 +741,21 @@ def test__validate_rules__with_rules__ok(mocker): account=account, name='Fieldset', ) - rule_1 = FieldsetTemplateRule.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( account=account, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='100', - ) - service = FieldSetTemplateService( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - instance=fieldset, - ) - - # mock - fieldset_template_rule_service_init_mock = mocker.patch.object( - FieldsetTemplateRuleService, - attribute='__init__', - return_value=None, + api_name='ruleset-1', ) - fieldset_template_rule_service_validate_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService._validate', - ) - - # act - service._validate_rules() - - # assert - fieldset_template_rule_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - instance=rule_1, - ) - fieldset_template_rule_service_validate_mock.assert_called_once_with() - - -def test_update_rules__existing_rule__ok(mocker): - - """ - Update existing rule - """ - - # arrange - account = create_test_account() - user = create_test_owner(account=account) - template = create_test_template(user=user, tasks_count=1) - fieldset = FieldsetTemplate.objects.create( - template=template, + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, account=account, - name='Fieldset', + api_name='group-or-1', ) - rule_1 = FieldsetTemplateRule.objects.create( + group_and = FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, value='100', ) service = FieldSetTemplateService( @@ -785,47 +764,43 @@ def test_update_rules__existing_rule__ok(mocker): auth_type=AuthTokenType.USER, instance=fieldset, ) - rules_data = [{'api_name': rule_1.api_name, 'value': '200'}] - # mock - fieldset_template_rule_service_init_mock = mocker.patch.object( - FieldsetTemplateRuleService, - attribute='__init__', - return_value=None, - ) - fieldset_template_rule_service_partial_update_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService.partial_update', + partial_update_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset.' + 'FieldsetTemplateRuleSetService.partial_update', ) - fieldset_template_rule_service_create_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService.create', + create_ruleset_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset.' + 'FieldSetTemplateService.create_ruleset', ) + ruleset_data = { + 'api_name': ruleset.api_name, + 'groups_or': [ + { + 'api_name': group_or.api_name, + 'groups_and': [ + { + 'api_name': group_and.api_name, + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '200', + }, + ], + }, + ], + } # act - service.update_rules(rules_data=rules_data) + service.update_rulesets(rulesets_data=[ruleset_data]) # assert - fieldset_template_rule_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - instance=rule_1, - ) - fs_rule_update_mock = ( - fieldset_template_rule_service_partial_update_mock - ) - fs_rule_update_mock.assert_called_once_with( - value='200', - api_name=rule_1.api_name, - ) - fieldset_template_rule_service_create_mock.assert_not_called() + partial_update_mock.assert_called_once_with(**ruleset_data) + create_ruleset_mock.assert_not_called() def test_update_rules__new_rule__ok(mocker): """ - Create new rule + Create new ruleset """ # arrange @@ -837,60 +812,56 @@ def test_update_rules__new_rule__ok(mocker): account=account, name='Fieldset', ) + rule_api_name = 'r-1' + group_or_api_name = 'gp-1' + group_and_api_name = 'ga-1' + + partial_update_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset.' + 'FieldsetTemplateRuleSetService.partial_update', + ) + ruleset_mock = mocker.Mock() + ruleset_mock.api_name = rule_api_name + create_ruleset_mock = mocker.patch( + 'src.processes.services.fieldsets.fieldset.' + 'FieldSetTemplateService.create_ruleset', + return_value=ruleset_mock, + ) service = FieldSetTemplateService( user=user, is_superuser=False, auth_type=AuthTokenType.USER, instance=fieldset, ) - rules_data = [ - { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '100', - 'api_name': 'rule-api-name', - }, - ] - # mock - create_return = mocker.Mock() - create_return.api_name = 'new-rule-api' - fs_rule_init_mock = mocker.patch.object( - FieldsetTemplateRuleService, - attribute='__init__', - return_value=None, - ) - fs_rule_create_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService.create', - return_value=create_return, - ) - fs_rule_update_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService.partial_update', - ) + ruleset_data = { + 'api_name': rule_api_name, + 'groups_or': [ + { + 'api_name': group_or_api_name, + 'groups_and': [ + { + 'api_name': group_and_api_name, + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], + } # act - service.update_rules(rules_data=rules_data) + service.update_rulesets(rulesets_data=[ruleset_data]) # assert - fs_rule_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - ) - fs_rule_create_mock.assert_called_once_with( - fieldset_id=fieldset.id, - type=FieldSetRuleType.SUM_EQUAL, - api_name='rule-api-name', - value='100', - ) - fs_rule_update_mock.assert_not_called() + partial_update_mock.assert_not_called() + create_ruleset_mock.assert_called_once_with(ruleset_data) -def test_update_rules__orphan_rules__deleted(mocker): +def test_update_rules__orphan_rules__deleted(): """ - Orphan rules deleted + Orphan rulesets deleted """ # arrange @@ -902,16 +873,38 @@ def test_update_rules__orphan_rules__deleted(mocker): account=account, name='Fieldset', ) - rule_1 = FieldsetTemplateRule.objects.create( + ruleset_1 = FieldSetTemplateRuleSet.objects.create( account=account, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + api_name='ruleset-1', + ) + group_or_1 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset_1, + account=account, + api_name='group-or-1', + ) + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or_1, + account=account, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, value='100', ) - rule_2 = FieldsetTemplateRule.objects.create( + ruleset_2 = FieldSetTemplateRuleSet.objects.create( account=account, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, + api_name='ruleset-2', + ) + group_or_2 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset_2, + account=account, + api_name='group-or-2', + ) + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or_2, + account=account, + api_name='group-and-2', + operator=FieldSetRuleOperator.SUM_EQUAL, value='200', ) service = FieldSetTemplateService( @@ -920,39 +913,42 @@ def test_update_rules__orphan_rules__deleted(mocker): auth_type=AuthTokenType.USER, instance=fieldset, ) - rules_data = [{'api_name': rule_1.api_name, 'value': '150'}] - - # mock - fs_rule_init_mock = mocker.patch.object( - FieldsetTemplateRuleService, - attribute='__init__', - return_value=None, - ) - fs_rule_update_mock = mocker.patch( - 'src.processes.services.fieldsets.fieldset_rule.' - 'FieldsetTemplateRuleService.partial_update', - ) + rulesets_data = [ + { + 'api_name': ruleset_1.api_name, + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '150', + }, + ], + }, + ], + }, + ] # act - service.update_rules(rules_data=rules_data) + service.update_rulesets(rulesets_data=rulesets_data) # assert - fs_rule_init_mock.assert_called_once() - fs_rule_update_mock.assert_called_once() - assert not FieldsetTemplateRule.objects.filter( - id=rule_2.id, + assert not FieldSetTemplateRuleSet.objects.filter( + id=ruleset_2.id, ).exists() - assert FieldsetTemplateRule.objects.filter( - id=rule_1.id, + assert FieldSetTemplateRuleSet.objects.filter( + id=ruleset_1.id, ).exists() + group_and = ruleset_1.groups_or.first().groups_and.first() + assert group_and.value == '150' def test_update_rules__two_rules_same_type_no_api_name__create_new_rule(): """ - Two rules with the same type in rules_data: + Two rules with the same type in rulesets_data: one existing (with api_name) and one new (without api_name). - Both rules must be persisted after update_rules completes. + Both rules must be persisted after update_rulesets completes. """ # arrange @@ -960,7 +956,8 @@ def test_update_rules__two_rules_same_type_no_api_name__create_new_rule(): user = create_test_owner(account=account) fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + rule_value='1', ) field_1 = fieldset.fields.first() field_2 = FieldTemplate.objects.create( @@ -970,9 +967,9 @@ def test_update_rules__two_rules_same_type_no_api_name__create_new_rule(): api_name='f2', fieldset=fieldset, ) - existing_rule = fieldset.rules.first() - existing_rule.fields.add(field_1) - existing_rule.fields.add(field_2) + existing_ruleset = fieldset.rulesets.first() + existing_ruleset.fields.add(field_1) + existing_ruleset.fields.add(field_2) service = FieldSetTemplateService( user=user, @@ -980,41 +977,60 @@ def test_update_rules__two_rules_same_type_no_api_name__create_new_rule(): auth_type=AuthTokenType.USER, instance=fieldset, ) - rules_data = [ + rulesets_data = [ { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '1', + 'api_name': existing_ruleset.api_name, 'fields': [field_1.api_name, field_2.api_name], - 'api_name': existing_rule.api_name, + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '1', + }, + ], + }, + ], }, { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '100', 'fields': [field_1.api_name, field_2.api_name], + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], }, ] # act - service.update_rules(rules_data=rules_data) + service.update_rulesets(rulesets_data=rulesets_data) # assert - assert fieldset.rules.count() == 2 - existing_rule.refresh_from_db() - assert existing_rule.value == '1' - assert existing_rule.type == FieldSetRuleType.SUM_EQUAL - assert existing_rule.fields.count() == 2 - assert existing_rule.fields.get(id=field_1.id) - assert existing_rule.fields.get(id=field_2.id) - - new_rule = fieldset.rules.exclude( - fieldset=fieldset, - api_name=existing_rule.api_name, + assert fieldset.rulesets.count() == 2 + existing_ruleset.refresh_from_db() + assert existing_ruleset.fields.count() == 2 + assert existing_ruleset.fields.get(id=field_1.id) + assert existing_ruleset.fields.get(id=field_2.id) + existing_group_and = ( + existing_ruleset.groups_or.first().groups_and.first() + ) + assert existing_group_and.operator == FieldSetRuleOperator.SUM_EQUAL + assert existing_group_and.value == '1' + + new_ruleset = fieldset.rulesets.exclude( + api_name=existing_ruleset.api_name, ).get() - assert new_rule.type == FieldSetRuleType.SUM_EQUAL - assert new_rule.value == '100' - assert new_rule.fields.count() == 2 - assert new_rule.fields.get(id=field_1.id) - assert new_rule.fields.get(id=field_2.id) + assert new_ruleset.fields.count() == 2 + assert new_ruleset.fields.get(id=field_1.id) + assert new_ruleset.fields.get(id=field_2.id) + new_group_and = new_ruleset.groups_or.first().groups_and.first() + assert new_group_and.operator == FieldSetRuleOperator.SUM_EQUAL + assert new_group_and.value == '100' def test_partial_update_name__ok(mocker): @@ -1036,11 +1052,7 @@ def test_partial_update_name__ok(mocker): ) mock_update_rules = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.update_rules', - ) - mock_validate_rules = mocker.patch( - 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService._validate_rules', + 'FieldSetTemplateService.update_rulesets', ) service = FieldSetTemplateService(instance=fieldset, user=owner) data = {"name": 'Updated Name'} @@ -1052,7 +1064,6 @@ def test_partial_update_name__ok(mocker): assert result.name == data['name'] mock_update_fields.assert_not_called() mock_update_rules.assert_not_called() - mock_validate_rules.assert_called_once_with() def test_partial_update_fields_ok(mocker): @@ -1075,11 +1086,7 @@ def test_partial_update_fields_ok(mocker): ) mock_update_rules = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.update_rules', - ) - mock_validate_rules = mocker.patch( - 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService._validate_rules', + 'FieldSetTemplateService.update_rulesets', ) mock_super_partial_update = mocker.patch( 'src.generics.base.service.' @@ -1098,11 +1105,10 @@ def test_partial_update_fields_ok(mocker): mock_super_partial_update.assert_not_called() mock_update_fields.assert_called_once_with(fields_data=data['fields']) mock_update_rules.assert_not_called() - mock_validate_rules.assert_called_once_with() -def test_partial_update__rules__ok(mocker): - """Verify rules update logic for existing and new rules""" +def test_partial_update__rulesets__ok(mocker): + """Verify rulesets update logic for existing and new rulesets""" # arrange account = create_test_account() @@ -1124,16 +1130,24 @@ def test_partial_update__rules__ok(mocker): ) mock_update_rules = mocker.patch( 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService.update_rules', - ) - mock_validate_rules = mocker.patch( - 'src.processes.services.fieldsets.fieldset.' - 'FieldSetTemplateService._validate_rules', + 'FieldSetTemplateService.update_rulesets', ) service = FieldSetTemplateService(user=owner, instance=fieldset) data = { - 'rules': [ - {"api_name": "rule_1", "condition": "eq"}, + 'rulesets': [ + { + 'api_name': 'ruleset-1', + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], + }, ], } @@ -1144,8 +1158,7 @@ def test_partial_update__rules__ok(mocker): assert result is fieldset mock_super_partial_update.assert_not_called() mock_update_fields.assert_not_called() - mock_update_rules.assert_called_once_with(rules_data=data['rules']) - mock_validate_rules.assert_called_once_with() + mock_update_rules.assert_called_once_with(rulesets_data=data['rulesets']) def test_delete__not_in_use__ok(): @@ -1406,19 +1419,41 @@ def test__replace_api_names__fields_and_rules__ok(mocker): 'selections': [{'api_name': 'old-selection-1', 'value': 'A'}], }, ], - 'rules': [{'api_name': 'old-rule-1', 'fields': [old_field_api]}], + 'rulesets': [ + { + 'api_name': 'old-rule-1', + 'fields': [old_field_api], + 'groups_or': [ + { + 'api_name': 'old-group-or-1', + 'groups_and': [ + { + 'api_name': 'old-group-and-1', + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], + }, + ], } new_fs_api = 'new-fs-1' new_field_api = 'new-field-1' new_selection_api = 'new-selection-1' - new_rule_api = 'new-rule-1' + new_ruleset_api = 'new-ruleset-1' + new_group_or_api = 'new-group-or-1' + new_group_and_api = 'new-group-and-1' create_api_name_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.create_api_name', + side_effect=[ new_fs_api, new_field_api, new_selection_api, - new_rule_api, + new_ruleset_api, + new_group_or_api, + new_group_and_api, ], ) @@ -1429,19 +1464,30 @@ def test__replace_api_names__fields_and_rules__ok(mocker): # assert assert result['api_name'] == new_fs_api - assert result['fields'][0]['api_name'] == new_field_api - assert ( - result['fields'][0]['selections'][0]['api_name'] == new_selection_api - ) - assert result['rules'][0]['api_name'] == new_rule_api - assert result['rules'][0]['fields'][0] == new_field_api - assert create_api_name_mock.call_count == 4 + field_data = result['fields'][0] + assert field_data['api_name'] == new_field_api + + selection_data = field_data['selections'][0] + assert selection_data['api_name'] == new_selection_api + + rule_data = result['rulesets'][0] + assert rule_data['api_name'] == new_ruleset_api + assert rule_data['fields'][0] == new_field_api + + group_or_data = rule_data['groups_or'][0] + assert group_or_data['api_name'] == new_group_or_api + + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == new_group_and_api + assert create_api_name_mock.call_count == 6 create_api_name_mock.assert_has_calls( [ mocker.call(FieldsetTemplate.api_name_prefix), mocker.call(FieldTemplate.api_name_prefix), mocker.call(FieldTemplateSelection.api_name_prefix), - mocker.call(FieldsetTemplateRule.api_name_prefix), + mocker.call(FieldSetTemplateRuleSet.api_name_prefix), + mocker.call(FieldSetTemplateRuleGroupOr.api_name_prefix), + mocker.call(FieldSetTemplateRuleGroupAnd.api_name_prefix), ], any_order=True, ) @@ -1537,7 +1583,7 @@ def test__replace_api_names__no_rules_key__ok(mocker): ) # assert - assert result['rules'] == [] + assert result['rulesets'] == [] create_api_name_mock.assert_called_once_with( FieldsetTemplate.api_name_prefix, ) @@ -1557,7 +1603,7 @@ def test__replace_api_names__empty_rules__ok(mocker): is_superuser=False, auth_type=AuthTokenType.USER, ) - shared_fieldset_data = {'api_name': 'old-fs', 'rules': []} + shared_fieldset_data = {'api_name': 'old-fs', 'rulesets': []} create_api_name_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.create_api_name', return_value='new-fs-1', @@ -1569,7 +1615,7 @@ def test__replace_api_names__empty_rules__ok(mocker): ) # assert - assert result['rules'] == [] + assert result['rulesets'] == [] create_api_name_mock.assert_called_once_with( FieldsetTemplate.api_name_prefix, ) @@ -1593,7 +1639,7 @@ def test__replace_api_names__original_not_mutated__ok(mocker): shared_fieldset_data = { 'api_name': 'old-fs', 'fields': [{'api_name': old_field_api, 'name': 'F 1'}], - 'rules': [], + 'rulesets': [], } create_api_name_mock = mocker.patch( 'src.processes.services.fieldsets.fieldset.create_api_name', @@ -2235,7 +2281,7 @@ def test__get_clone__ok(mocker): 'label_position': fieldset.label_position, 'layout': fieldset.layout, 'fields': [], - 'rules': [], + 'rulesets': [], } clone_data = dict(instance_data) clone_data['api_name'] = 'new-fs-api' diff --git a/backend/src/processes/tests/test_services/test_tasks/test_task_service.py b/backend/src/processes/tests/test_services/test_tasks/test_task_service.py index 379973062..8a1f6d1f5 100644 --- a/backend/src/processes/tests/test_services/test_tasks/test_task_service.py +++ b/backend/src/processes/tests/test_services/test_tasks/test_task_service.py @@ -1685,7 +1685,7 @@ def test_create_conditions_from_template__no_conditions__ok(mocker): workflow = create_test_workflow(user=user, template=template) task = workflow.tasks.get(number=1) create_rules_mock = mocker.patch( - 'src.processes.services.tasks.mixins.ConditionMixin.create_rules', + 'src.processes.services.tasks.mixins.ConditionMixin.create_rulesets', ) service = TaskService(user=user, instance=task) @@ -1711,7 +1711,7 @@ def test_create_conditions_from_template__with_conditions__ok(mocker): workflow = create_test_workflow(user=user, template=template) task = workflow.tasks.get(number=2) create_rules_mock = mocker.patch( - 'src.processes.services.tasks.mixins.ConditionMixin.create_rules', + 'src.processes.services.tasks.mixins.ConditionMixin.create_rulesets', ) service = TaskService(user=user, instance=task) diff --git a/backend/src/processes/tests/test_services/test_tasks/test_task_version_service.py b/backend/src/processes/tests/test_services/test_tasks/test_task_version_service.py index 36015be55..4a54853a3 100644 --- a/backend/src/processes/tests/test_services/test_tasks/test_task_version_service.py +++ b/backend/src/processes/tests/test_services/test_tasks/test_task_version_service.py @@ -11,7 +11,7 @@ FieldType, PerformerType, PredicateOperator, - WorkflowStatus, + WorkflowStatus, FieldSetRuleOperator, ) from src.processes.models.workflows.fieldset import FieldSet, FieldSetRule from src.processes.models.workflows.fields import FieldSelection, TaskField @@ -42,7 +42,6 @@ from src.processes.enums import ( FieldSetLayout, - FieldSetRuleType, LabelPosition, ) @@ -2226,7 +2225,6 @@ def test__update_fieldset_rules__rules_data_none__skip(): existing_rule = FieldSetRule.objects.create( fieldset=fieldset, account_id=user.account_id, - type=FieldSetRuleType.SUM_EQUAL, value='100', api_name='rule-1', ) @@ -2261,7 +2259,6 @@ def test__update_fieldset_rules__rules_data_empty__skip(): existing_rule = FieldSetRule.objects.create( fieldset=fieldset, account_id=user.account_id, - type=FieldSetRuleType.SUM_EQUAL, value='100', api_name='rule-1', ) @@ -2296,7 +2293,6 @@ def test__update_fieldset_rules__rules_data_provided__ok(): old_rule = FieldSetRule.objects.create( fieldset=fieldset, account_id=user.account_id, - type=FieldSetRuleType.SUM_EQUAL, value='50', api_name='old-rule-1', ) @@ -2309,7 +2305,6 @@ def test__update_fieldset_rules__rules_data_provided__ok(): rules_data = [ { 'api_name': 'rule-1', - 'type': FieldSetRuleType.SUM_EQUAL, 'value': '100', }, ] @@ -2322,7 +2317,6 @@ def test__update_fieldset_rules__rules_data_provided__ok(): assert FieldSetRule.objects.filter( fieldset=fieldset, api_name='rule-1', - type=FieldSetRuleType.SUM_EQUAL, value='100', ).exists() @@ -2626,7 +2620,6 @@ def test__update_fieldsets__data_provided__ok(mocker): 'rules': [ { 'api_name': 'rule-1', - 'type': FieldSetRuleType.SUM_EQUAL, 'value': '100', }, ], @@ -2691,10 +2684,10 @@ def test__update_field_rules__rules_provided__ok(): workflow=workflow, task=task, api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field = fieldset.fields.first() service = TaskUpdateVersionService( user=user, @@ -2712,8 +2705,8 @@ def test__update_field_rules__rules_provided__ok(): service._update_field_rules(field, field_data, fieldset) # assert - assert field.rules.count() == 1 - assert field.rules.filter(id=rule.id).exists() + assert field.rulesets.count() == 1 + assert field.rulesets.filter(id=rule.id).exists() def test__update_field_rules__rules_empty_list__clear(): @@ -2731,12 +2724,12 @@ def test__update_field_rules__rules_empty_list__clear(): workflow=workflow, task=task, api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.get(api_name='fs-1-rule-1') + rule = fieldset.rulesets.get(api_name='fs-1-rule-1') field = fieldset.fields.get(api_name='fs-1-field-1') - field.rules.add(rule) + field.rulesets.add(rule) service = TaskUpdateVersionService( user=user, instance=task, @@ -2749,7 +2742,7 @@ def test__update_field_rules__rules_empty_list__clear(): service._update_field_rules(field, field_data, fieldset) # assert - assert field.rules.count() == 0 + assert field.rulesets.count() == 0 def test__update_field_rules__rules_key_missing__clear(): @@ -2767,12 +2760,12 @@ def test__update_field_rules__rules_key_missing__clear(): workflow=workflow, task=task, api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.get(api_name='fs-1-rule-1') + rule = fieldset.rulesets.get(api_name='fs-1-rule-1') field = fieldset.fields.get(api_name='fs-1-field-1') - field.rules.add(rule) + field.rulesets.add(rule) service = TaskUpdateVersionService( user=user, instance=task, @@ -2785,7 +2778,7 @@ def test__update_field_rules__rules_key_missing__clear(): service._update_field_rules(field, field_data, fieldset) # assert - assert field.rules.count() == 0 + assert field.rulesets.count() == 0 def test__update_field_rules__multiple_rules__ok(): @@ -2803,14 +2796,13 @@ def test__update_field_rules__multiple_rules__ok(): workflow=workflow, task=task, api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule_1 = fieldset.rules.get(api_name='fs-1-rule-1') + rule_1 = fieldset.rulesets.get(api_name='fs-1-rule-1') rule_2 = FieldSetRule.objects.create( fieldset=fieldset, account_id=user.account_id, - type=FieldSetRuleType.SUM_EQUAL, value='200', api_name='fs-1-rule-2', ) @@ -2832,9 +2824,9 @@ def test__update_field_rules__multiple_rules__ok(): service._update_field_rules(field, field_data, fieldset) # assert - assert field.rules.count() == 2 - assert field.rules.filter(id=rule_1.id).exists() - assert field.rules.filter(id=rule_2.id).exists() + assert field.rulesets.count() == 2 + assert field.rulesets.filter(id=rule_1.id).exists() + assert field.rulesets.filter(id=rule_2.id).exists() def test__update_field_rules__replaces_existing_rules__ok(): @@ -2851,19 +2843,18 @@ def test__update_field_rules__replaces_existing_rules__ok(): workflow=workflow, task=task, api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='50', ) - old_rule = fieldset.rules.get(api_name='fs-1-rule-1') + old_rule = fieldset.rulesets.get(api_name='fs-1-rule-1') new_rule = FieldSetRule.objects.create( fieldset=fieldset, account_id=user.account_id, - type=FieldSetRuleType.SUM_EQUAL, value='100', api_name='new-rule', ) field = fieldset.fields.get(api_name='fs-1-field-1') - field.rules.add(old_rule) + field.rulesets.add(old_rule) service = TaskUpdateVersionService( user=user, instance=task, @@ -2876,9 +2867,9 @@ def test__update_field_rules__replaces_existing_rules__ok(): service._update_field_rules(field, field_data, fieldset) # assert - assert field.rules.count() == 1 - assert field.rules.filter(id=new_rule.id).exists() - assert not field.rules.filter(id=old_rule.id).exists() + assert field.rulesets.count() == 1 + assert field.rulesets.filter(id=new_rule.id).exists() + assert not field.rulesets.filter(id=old_rule.id).exists() def test__update_field_rules__nonexistent_api_name__skip(): @@ -2910,7 +2901,7 @@ def test__update_field_rules__nonexistent_api_name__skip(): service._update_field_rules(field, field_data, fieldset) # assert - assert field.rules.count() == 0 + assert field.rulesets.count() == 0 def test_update_from_version__tasks_fields_values_include_fieldset_fields__ok( diff --git a/backend/src/processes/tests/test_services/test_tasks/test_taskfield.py b/backend/src/processes/tests/test_services/test_tasks/test_taskfield.py index e980348b1..9db063ded 100644 --- a/backend/src/processes/tests/test_services/test_tasks/test_taskfield.py +++ b/backend/src/processes/tests/test_services/test_tasks/test_taskfield.py @@ -3,7 +3,7 @@ from django.test import override_settings from src.processes.enums import ( - FieldSetRuleType, + FieldSetRuleOperator, FieldType, WorkflowEventType, ) @@ -13,7 +13,9 @@ FieldTemplateSelection, ) from src.processes.models.templates.fieldset import ( - FieldsetTemplateRule, + FieldSetTemplateRuleGroupAnd, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleSet, ) from src.processes.models.workflows.fieldset import ( FieldSetRule, @@ -1058,11 +1060,11 @@ def test__create_related__with_rules__ok(mocker): account=account, template=template, task=task_template, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) field_template = fieldset_template.fields.first() - rule_template = fieldset_template.rules.first() - rule_template.fields.add(field_template) + ruleset_template = fieldset_template.rulesets.first() + ruleset_template.fields.add(field_template) workflow = create_test_workflow(user=user, template=template) task = workflow.tasks.get(number=1) task_field = TaskField.objects.create( @@ -2645,7 +2647,7 @@ def test__get_valid_value__not_required_and_null_value__ok( def test__link_rules__one_rule__ok(): - """One template rule → one FieldSetRule linked""" + """One template ruleset → one FieldSetRule linked""" # arrange account = create_test_account() @@ -2661,11 +2663,11 @@ def test__link_rules__one_rule__ok(): template=template, task=task_template, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) field_template = fieldset_template.fields.first() - rule_template = fieldset_template.rules.first() - rule_template.fields.add(field_template) + ruleset_template = fieldset_template.rulesets.first() + ruleset_template.fields.add(field_template) workflow = create_test_workflow( user=user, @@ -2676,9 +2678,12 @@ def test__link_rules__one_rule__ok(): workflow=workflow, task=task, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) - rule = fieldset.rules.first() + # Keep runtime rule api_name in sync with template ruleset + rule = fieldset.rulesets.first() + rule.api_name = ruleset_template.api_name + rule.save(update_fields=['api_name']) task_field = fieldset.fields.first() service = TaskFieldService( @@ -2693,13 +2698,13 @@ def test__link_rules__one_rule__ok(): ) # assert - assert task_field.rules.count() == 1 - assert task_field.rules.first() == rule + assert task_field.rulesets.count() == 1 + assert task_field.rulesets.first() == rule def test__link_rules__multiple_rules__ok(): - """Two template rules → two FieldSetRules linked""" + """Two template rulesets → two FieldSetRules linked""" # arrange account = create_test_account() @@ -2715,22 +2720,29 @@ def test__link_rules__multiple_rules__ok(): template=template, task=task_template, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) - rule_tmpl_2 = FieldsetTemplateRule.objects.create( + ruleset_tmpl_1 = fieldset_template.rulesets.first() + ruleset_tmpl_2 = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset_template, account=account, - api_name=f'{fieldset_api_name}-rule-2', - type=FieldSetRuleType.SUM_EQUAL, - value='200', + api_name=f'{fieldset_api_name}-ruleset-2', ) - field_template = fieldset_template.fields.first() - rule_tmpl_1 = fieldset_template.rules.get( - api_name=f'{fieldset_api_name}-rule-1', + group_or_2 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset_tmpl_2, + account=account, + api_name=f'{fieldset_api_name}-group-or-2', ) - field_template.rules.set( - [rule_tmpl_1, rule_tmpl_2], + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or_2, + account=account, + api_name=f'{fieldset_api_name}-group-and-2', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='200', ) + field_template = fieldset_template.fields.first() + ruleset_tmpl_1.fields.add(field_template) + ruleset_tmpl_2.fields.add(field_template) workflow = create_test_workflow( user=user, @@ -2741,16 +2753,17 @@ def test__link_rules__multiple_rules__ok(): workflow=workflow, task=task, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) + rule_1 = fieldset.rulesets.first() + rule_1.api_name = ruleset_tmpl_1.api_name + rule_1.save(update_fields=['api_name']) rule_2 = FieldSetRule.objects.create( fieldset=fieldset, account=account, - api_name=f'{fieldset_api_name}-rule-2', - type=FieldSetRuleType.SUM_EQUAL, + api_name=ruleset_tmpl_2.api_name, value='200', ) - rule_1 = fieldset.rules.exclude(id=rule_2.id).first() task_field = fieldset.fields.first() service = TaskFieldService( @@ -2765,16 +2778,16 @@ def test__link_rules__multiple_rules__ok(): ) # assert - assert task_field.rules.count() == 2 + assert task_field.rulesets.count() == 2 linked_ids = set( - task_field.rules.values_list('id', flat=True), + task_field.rulesets.values_list('id', flat=True), ) assert linked_ids == {rule_1.id, rule_2.id} def test__link_rules__partial_match__ok(): - """Two template rules, only one FieldSetRule exists + """Two template rulesets, only one FieldSetRule exists — only matched one linked""" # arrange @@ -2791,22 +2804,29 @@ def test__link_rules__partial_match__ok(): template=template, task=task_template, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) - rule_tmpl_2 = FieldsetTemplateRule.objects.create( + ruleset_tmpl_1 = fieldset_template.rulesets.first() + ruleset_tmpl_2 = FieldSetTemplateRuleSet.objects.create( fieldset=fieldset_template, account=account, - api_name=f'{fieldset_api_name}-rule-2', - type=FieldSetRuleType.SUM_EQUAL, - value='200', + api_name=f'{fieldset_api_name}-ruleset-2', ) - field_template = fieldset_template.fields.first() - rule_tmpl_1 = fieldset_template.rules.get( - api_name=f'{fieldset_api_name}-rule-1', + group_or_2 = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset_tmpl_2, + account=account, + api_name=f'{fieldset_api_name}-group-or-2', ) - field_template.rules.set( - [rule_tmpl_1, rule_tmpl_2], + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or_2, + account=account, + api_name=f'{fieldset_api_name}-group-and-2', + operator=FieldSetRuleOperator.SUM_EQUAL, + value='200', ) + field_template = fieldset_template.fields.first() + ruleset_tmpl_1.fields.add(field_template) + ruleset_tmpl_2.fields.add(field_template) workflow = create_test_workflow( user=user, @@ -2817,9 +2837,11 @@ def test__link_rules__partial_match__ok(): workflow=workflow, task=task, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() + rule.api_name = ruleset_tmpl_1.api_name + rule.save(update_fields=['api_name']) task_field = fieldset.fields.first() service = TaskFieldService( @@ -2834,13 +2856,13 @@ def test__link_rules__partial_match__ok(): ) # assert - assert task_field.rules.count() == 1 - assert task_field.rules.first() == rule + assert task_field.rulesets.count() == 1 + assert task_field.rulesets.first() == rule def test__link_rules__no_matching_rules__empty(): - """Template has rule, but no FieldSetRule + """Template has ruleset, but no FieldSetRule with that api_name — M2M stays empty""" # arrange @@ -2857,11 +2879,11 @@ def test__link_rules__no_matching_rules__empty(): template=template, task=task_template, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) field_template = fieldset_template.fields.first() - rule_template = fieldset_template.rules.first() - rule_template.fields.add(field_template) + ruleset_template = fieldset_template.rulesets.first() + ruleset_template.fields.add(field_template) workflow = create_test_workflow( user=user, @@ -2877,7 +2899,6 @@ def test__link_rules__no_matching_rules__empty(): fieldset=fieldset, account=account, api_name='different-rule', - type=FieldSetRuleType.SUM_EQUAL, value='999', ) task_field = fieldset.fields.first() @@ -2894,7 +2915,7 @@ def test__link_rules__no_matching_rules__empty(): ) # assert - assert task_field.rules.count() == 0 + assert task_field.rulesets.count() == 0 def test__link_rules__another_fieldset_rule__not_linked(): @@ -2916,11 +2937,11 @@ def test__link_rules__another_fieldset_rule__not_linked(): template=template, task=task_template, api_name=fieldset_api_name, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) field_template = fieldset_template.fields.first() - rule_template = fieldset_template.rules.first() - rule_template.fields.add(field_template) + ruleset_template = fieldset_template.rulesets.first() + ruleset_template.fields.add(field_template) workflow = create_test_workflow( user=user, @@ -2933,12 +2954,15 @@ def test__link_rules__another_fieldset_rule__not_linked(): api_name=fieldset_api_name, ) task_field = fieldset_1.fields.first() - create_test_fieldset( + fieldset_2 = create_test_fieldset( workflow=workflow, task=task, api_name='fs2', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) + rule_2 = fieldset_2.rulesets.first() + rule_2.api_name = ruleset_template.api_name + rule_2.save(update_fields=['api_name']) service = TaskFieldService( instance=task_field, @@ -2952,4 +2976,4 @@ def test__link_rules__another_fieldset_rule__not_linked(): ) # assert - assert task_field.rules.count() == 0 + assert task_field.rulesets.count() == 0 diff --git a/backend/src/processes/tests/test_services/test_workflows/test_fieldset_rule_service.py b/backend/src/processes/tests/test_services/test_workflows/test_fieldset_rule_service.py index 91a41af6d..d98cc37c6 100644 --- a/backend/src/processes/tests/test_services/test_workflows/test_fieldset_rule_service.py +++ b/backend/src/processes/tests/test_services/test_workflows/test_fieldset_rule_service.py @@ -2,8 +2,8 @@ from src.authentication.enums import AuthTokenType from src.processes.enums import ( - FieldSetRuleType, FieldType, + FieldSetRuleOperator, ) from src.processes.messages import fieldset as fs_messages from src.processes.models.templates.fieldset import ( @@ -50,7 +50,6 @@ def test__create_instance__with_template__ok(): rule_template = FieldsetTemplateRule.objects.create( account=account, fieldset=fieldset_template, - type=FieldSetRuleType.SUM_EQUAL, value='100', ) service = FieldSetRuleService( @@ -68,7 +67,6 @@ def test__create_instance__with_template__ok(): # assert assert service.instance is not None assert service.instance.fieldset_id == fieldset.id - assert service.instance.type == FieldSetRuleType.SUM_EQUAL assert service.instance.value == '100' assert service.instance.api_name == rule_template.api_name assert service.instance.account_id == account.id @@ -89,10 +87,10 @@ def test__validate_sum_equal__within_threshold__ok(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -102,7 +100,7 @@ def test__validate_sum_equal__within_threshold__ok(): value='30', order=1, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -112,7 +110,7 @@ def test__validate_sum_equal__within_threshold__ok(): value='70', order=2, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -145,10 +143,10 @@ def test__validate_sum_equal__decimal_precision__ok(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='0.3', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -158,7 +156,7 @@ def test__validate_sum_equal__decimal_precision__ok(): value='0.1', order=1, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -168,7 +166,7 @@ def test__validate_sum_equal__decimal_precision__ok(): value='0.2', order=2, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -197,10 +195,10 @@ def test__validate_sum_equal__one_not_required_field_blank__not_count(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -211,7 +209,7 @@ def test__validate_sum_equal__one_not_required_field_blank__not_count(): order=1, is_required=False, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -222,7 +220,7 @@ def test__validate_sum_equal__one_not_required_field_blank__not_count(): order=2, is_required=True, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -251,10 +249,10 @@ def test__validate_sum_equal__all_not_required_fields_blank__not_count(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -265,7 +263,7 @@ def test__validate_sum_equal__all_not_required_fields_blank__not_count(): order=1, is_required=False, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -276,7 +274,7 @@ def test__validate_sum_equal__all_not_required_fields_blank__not_count(): order=2, is_required=False, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -305,10 +303,10 @@ def test__validate_sum_equal__required_field_blank__raise_exception(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -319,7 +317,7 @@ def test__validate_sum_equal__required_field_blank__raise_exception(): order=1, is_required=True, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -353,10 +351,10 @@ def test__validate_sum_equal__negative_value__ok(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='0', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -366,7 +364,7 @@ def test__validate_sum_equal__negative_value__ok(): value='30', order=1, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -376,7 +374,7 @@ def test__validate_sum_equal__negative_value__ok(): value='-30', order=2, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -409,10 +407,10 @@ def test__validate_sum_equal__exceeds__raise_exception(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -422,7 +420,7 @@ def test__validate_sum_equal__exceeds__raise_exception(): value='60', order=1, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -432,7 +430,7 @@ def test__validate_sum_equal__exceeds__raise_exception(): value='50', order=2, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -466,10 +464,10 @@ def test__validate_sum_equal__null_values__skip(): workflow=workflow, name='Fieldset', order=1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() field_1 = TaskField.objects.create( account=account, workflow=workflow, @@ -479,7 +477,7 @@ def test__validate_sum_equal__null_values__skip(): value='100', order=1, ) - field_1.rules.add(rule) + field_1.rulesets.add(rule) field_2 = TaskField.objects.create( account=account, workflow=workflow, @@ -489,7 +487,7 @@ def test__validate_sum_equal__null_values__skip(): value='', order=2, ) - field_2.rules.add(rule) + field_2.rulesets.add(rule) service = FieldSetRuleService( user=user, is_superuser=False, @@ -520,9 +518,9 @@ def test_validate__ok(mocker): fieldset = create_test_fieldset( workflow=workflow, kickoff=workflow.kickoff_instance, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() service = FieldSetRuleService( instance=rule, user=user, diff --git a/backend/src/processes/tests/test_services/test_workflows/test_fieldset_service.py b/backend/src/processes/tests/test_services/test_workflows/test_fieldset_service.py index affcf9333..6e128a17f 100644 --- a/backend/src/processes/tests/test_services/test_workflows/test_fieldset_service.py +++ b/backend/src/processes/tests/test_services/test_workflows/test_fieldset_service.py @@ -1,7 +1,7 @@ import pytest from src.authentication.enums import AuthTokenType from src.processes.enums import ( - FieldSetRuleType, + FieldSetRuleOperator, FieldType, ) from src.processes.messages.fieldset import ( @@ -11,7 +11,9 @@ ) from src.processes.models.templates.fieldset import ( FieldsetTemplate, - FieldsetTemplateRule, + FieldSetTemplateRuleSet, + FieldSetTemplateRuleGroupOr, + FieldSetTemplateRuleGroupAnd, ) from src.processes.models.templates.fields import FieldTemplate from src.processes.models.workflows.fieldset import ( @@ -368,10 +370,21 @@ def test__create_rules__with_template__ok(mocker): account=account, name='Fieldset', ) - rule_template = FieldsetTemplateRule.objects.create( + ruleset = FieldSetTemplateRuleSet.objects.create( account=account, fieldset=fieldset_template, - type=FieldSetRuleType.SUM_EQUAL, + api_name='ruleset-1', + ) + group_or = FieldSetTemplateRuleGroupOr.objects.create( + fieldset_rule=ruleset, + account=account, + api_name='group-or-1', + ) + FieldSetTemplateRuleGroupAnd.objects.create( + group_or=group_or, + account=account, + api_name='group-and-1', + operator=FieldSetRuleOperator.SUM_EQUAL, value='100', ) fieldset = create_test_fieldset( @@ -400,11 +413,13 @@ def test__create_rules__with_template__ok(mocker): field_set_rule_service_init_mock.assert_called_once_with( user=user, ) - field_set_rule_service_create_mock.assert_called_once_with( - instance_template=rule_template, - fieldset=fieldset, - skip_validation=None, - ) + field_set_rule_service_create_mock.assert_called_once() + _, create_kwargs = field_set_rule_service_create_mock.call_args + assert create_kwargs['fieldset'] == fieldset + assert create_kwargs['skip_validation'] is None + runtime_rule = create_kwargs['instance_template'] + assert runtime_rule.api_name == ruleset.api_name + assert runtime_rule.value == '100' def test__create_related__with_template__ok(mocker): @@ -461,10 +476,10 @@ def test_validate_rules__one_rule__ok(mocker): workflow = create_test_workflow(user=user, template=template) fieldset = create_test_fieldset( workflow=workflow, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() service = FieldSetService( user=user, is_superuser=False, @@ -505,11 +520,11 @@ def test_validate_rules__one_rule_none_matches__raise_exception(mocker): workflow = create_test_workflow(user=user, template=template) fieldset = create_test_fieldset( workflow=workflow, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) field = fieldset.fields.first() - rule_100 = fieldset.rules.first() + rule_100 = fieldset.rulesets.first() rule_100.fields.add(field) service = FieldSetService( user=user, @@ -539,16 +554,15 @@ def test_validate_rules__two_same_type_rules__first_value_matches__ok(): workflow = create_test_workflow(user=user) fieldset = create_test_fieldset( workflow=workflow, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='10', ) field = fieldset.fields.first() - rule_10 = fieldset.rules.first() + rule_10 = fieldset.rulesets.first() rule_10.fields.add(field) rule_0 = FieldSetRule.objects.create( account=account, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, value='0', ) rule_0.fields.add(field) @@ -579,16 +593,15 @@ def test_validate_rules__two_same_type_rules__second_value_matches__ok(): workflow = create_test_workflow(user=user) fieldset = create_test_fieldset( workflow=workflow, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) field = fieldset.fields.first() - rule_100 = fieldset.rules.first() + rule_100 = fieldset.rulesets.first() rule_100.fields.add(field) rule_10 = FieldSetRule.objects.create( account=account, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, value='10', ) rule_10.fields.add(field) @@ -621,16 +634,15 @@ def test_validate_rules__two_same_type_rules__none_matches__raise(): workflow = create_test_workflow(user=user) fieldset = create_test_fieldset( workflow=workflow, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) field = fieldset.fields.first() - rule_100 = fieldset.rules.first() + rule_100 = fieldset.rulesets.first() rule_100.fields.add(field) rule_0 = FieldSetRule.objects.create( account=account, fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, value='0', ) rule_0.fields.add(field) diff --git a/backend/src/processes/tests/test_services/test_workflows/test_kickoff_version_service.py b/backend/src/processes/tests/test_services/test_workflows/test_kickoff_version_service.py index a83978525..2368e6623 100644 --- a/backend/src/processes/tests/test_services/test_workflows/test_kickoff_version_service.py +++ b/backend/src/processes/tests/test_services/test_workflows/test_kickoff_version_service.py @@ -3,9 +3,8 @@ from src.authentication.enums import AuthTokenType from src.processes.enums import ( FieldSetLayout, - FieldSetRuleType, FieldType, - LabelPosition, + LabelPosition, FieldSetRuleOperator, ) from src.processes.models.workflows.fields import ( FieldSelection, @@ -298,10 +297,10 @@ def test__update_fieldset_rules__rules_data_is_none__delete_all(): kickoff=kickoff, name='FS', api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - existing_rule = fieldset.rules.first() + existing_rule = fieldset.rulesets.first() service = KickoffUpdateVersionService( user=user, auth_type=AuthTokenType.USER, @@ -316,7 +315,7 @@ def test__update_fieldset_rules__rules_data_is_none__delete_all(): ) # assert - assert fieldset.rules.count() == 0 + assert fieldset.rulesets.count() == 0 assert FieldSetRule.objects.filter( id=existing_rule.id, ).exists() is False @@ -346,12 +345,12 @@ def test__update_fieldset_rules__provided__ok(): kickoff=kickoff, name='FS', api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='50', ) # stale rule to be deleted - stale_rule = fieldset.rules.first() + stale_rule = fieldset.rulesets.first() service = KickoffUpdateVersionService( user=user, auth_type=AuthTokenType.USER, @@ -361,7 +360,6 @@ def test__update_fieldset_rules__provided__ok(): rules_data = [ { 'api_name': 'rule-1', - 'type': FieldSetRuleType.SUM_EQUAL, 'value': '100', }, ] @@ -373,10 +371,9 @@ def test__update_fieldset_rules__provided__ok(): ) # assert - rules = fieldset.rules.all() + rules = fieldset.rulesets.all() assert rules.count() == 1 assert rules[0].api_name == 'rule-1' - assert rules[0].type == FieldSetRuleType.SUM_EQUAL assert rules[0].value == '100' assert rules[0].account_id == account.id assert FieldSetRule.objects.filter( @@ -408,10 +405,10 @@ def test__update_field_rules__provided__ok(): kickoff=kickoff, name='FS', api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule_1 = fieldset.rules.first() + rule_1 = fieldset.rulesets.first() field = TaskField.objects.create( kickoff=kickoff, workflow=workflow, @@ -442,8 +439,8 @@ def test__update_field_rules__provided__ok(): ) # assert - assert field.rules.count() == 1 - assert field.rules.filter(id=rule_1.id).exists() is True + assert field.rulesets.count() == 1 + assert field.rulesets.filter(id=rule_1.id).exists() is True def test__update_field_rules__empty__clear(): @@ -470,10 +467,10 @@ def test__update_field_rules__empty__clear(): kickoff=kickoff, name='FS', api_name='fs-1', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - rule_1 = fieldset.rules.first() + rule_1 = fieldset.rulesets.first() field = TaskField.objects.create( kickoff=kickoff, workflow=workflow, @@ -484,7 +481,7 @@ def test__update_field_rules__empty__clear(): type=FieldType.NUMBER, order=1, ) - field.rules.add(rule_1) + field.rulesets.add(rule_1) service = KickoffUpdateVersionService( user=user, auth_type=AuthTokenType.USER, @@ -501,7 +498,7 @@ def test__update_field_rules__empty__clear(): ) # assert - assert field.rules.count() == 0 + assert field.rulesets.count() == 0 def test__update_fields__provided__ok(mocker): @@ -846,7 +843,6 @@ def test__update_fieldsets__provided__ok(mocker): rules_data_1 = [ { 'api_name': 'rule-1', - 'type': FieldSetRuleType.SUM_EQUAL, 'value': '100', }, ] diff --git a/backend/src/processes/tests/test_services/test_workflows/test_workflow_version_service.py b/backend/src/processes/tests/test_services/test_workflows/test_workflow_version_service.py index 238d7ce68..37290b74e 100644 --- a/backend/src/processes/tests/test_services/test_workflows/test_workflow_version_service.py +++ b/backend/src/processes/tests/test_services/test_workflows/test_workflow_version_service.py @@ -298,8 +298,8 @@ def test_update_from_version__all_instances__ok(self): condition = task_1.conditions.get( action=ConditionAction.SKIP_TASK, ) - assert condition.rules.count() == 1 - rule = condition.rules.get() + assert condition.rulesets.count() == 1 + rule = condition.rulesets.get() assert rule.predicates.count() == 1 assert rule.predicates.get( operator=PredicateOperator.EQUAL, diff --git a/backend/src/processes/tests/test_views/test_fieldsets/test_clone.py b/backend/src/processes/tests/test_views/test_fieldsets/test_clone.py index c27c311e9..5d71a2a0e 100644 --- a/backend/src/processes/tests/test_views/test_fieldsets/test_clone.py +++ b/backend/src/processes/tests/test_views/test_fieldsets/test_clone.py @@ -9,7 +9,7 @@ from src.generics.exceptions import BaseServiceException from src.processes.enums import ( FieldSetLayout, - FieldSetRuleType, + FieldSetRuleOperator, LabelPosition, ) from src.processes.services.fieldsets.fieldset import ( @@ -38,12 +38,12 @@ def test_clone__ok(api_client, mocker): description='Original description', label_position=LabelPosition.LEFT, layout=FieldSetLayout.HORIZONTAL, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='10', ) field = fieldset.fields.get() - rule = fieldset.rules.get() - rule.fields.add(field) + ruleset = fieldset.rulesets.get() + ruleset.fields.add(field) clone = create_test_shared_fieldset( account=account, name=fieldset.name, @@ -51,13 +51,14 @@ def test_clone__ok(api_client, mocker): description=fieldset.description, label_position=fieldset.label_position, layout=fieldset.layout, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='10', api_name='cloned-fs', ) clone_field = clone.fields.get() - clone_rule = clone.rules.get() - clone_rule.fields.add(clone_field) + clone_ruleset = clone.rulesets.get() + clone_ruleset.fields.add(clone_field) + clone_group_and = clone_ruleset.groups_or.first().groups_and.first() field_set_template_service_init_mock = mocker.patch.object( FieldSetTemplateService, @@ -89,9 +90,16 @@ def test_clone__ok(api_client, mocker): assert response.data['fields'][0]['name'] == clone_field.name assert response.data['fields'][0]['api_name'] == clone_field.api_name - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['type'] == clone_rule.type - assert response.data['rules'][0]['api_name'] == clone_rule.api_name + assert len(response.data['rulesets']) == 1 + rule_resp = response.data['rulesets'][0] + assert rule_resp['api_name'] == clone_ruleset.api_name + assert rule_resp['fields'] == [clone_field.api_name] + assert rule_resp['groups_or'][0]['groups_and'][0]['operator'] == ( + FieldSetRuleOperator.SUM_EQUAL + ) + assert rule_resp['groups_or'][0]['groups_and'][0]['value'] == ( + clone_group_and.value + ) field_set_template_service_init_mock.assert_called_once_with( user=user, diff --git a/backend/src/processes/tests/test_views/test_fieldsets/test_create.py b/backend/src/processes/tests/test_views/test_fieldsets/test_create.py index 721b7ac55..a2c6d0a3e 100644 --- a/backend/src/processes/tests/test_views/test_fieldsets/test_create.py +++ b/backend/src/processes/tests/test_views/test_fieldsets/test_create.py @@ -8,15 +8,13 @@ from src.authentication.enums import AuthTokenType from src.generics.exceptions import BaseServiceException from src.processes.enums import ( + FieldRuleOperator, + FieldRuleType, FieldSetLayout, + FieldSetRuleOperator, + FieldType, LabelPosition, - FieldSetRuleType, FieldType, ) -from src.processes.models.templates.fieldset import ( - FieldsetTemplate, - FieldsetTemplateRule, -) -from src.processes.models.templates.fields import FieldTemplate from src.processes.services.fieldsets.fieldset import ( FieldSetTemplateService, @@ -26,27 +24,17 @@ create_test_admin, create_test_not_admin, create_test_owner, - create_test_template, + create_test_shared_fieldset, ) pytestmark = pytest.mark.django_db -def test_create_fieldset__all_fields__ok(api_client, mocker): - - """ - Create fieldset with all fields in request - and check all fields in response - """ +def test_create__all_fields__ok(api_client, mocker): # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template( - user=user, - tasks_count=1, - ) - task = template.tasks.first() data = { 'name': 'All Fields Fieldset', @@ -63,44 +51,43 @@ def test_create_fieldset__all_fields__ok(api_client, mocker): 'api_name': 'f1', }, ], - 'rules': [ + 'rulesets': [ { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '10', 'api_name': 'r1', + 'order': 1, 'fields': [], + 'groups_or': [ + { + 'api_name': 'g-or-1', + 'groups_and': [ + { + 'api_name': 'g-and-1', + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '10', + }, + ], + }, + ], }, ], } - fieldset = FieldsetTemplate.objects.create( + fieldset = create_test_shared_fieldset( account=account, - template=template, name=data['name'], title=data['title'], description=data['description'], label_position=data['label_position'], layout=data['layout'], api_name=data['api_name'], + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + rule_value='10', ) - field = FieldTemplate.objects.create( - account=account, - template=template, - task=task, - name='Field 1', - type='text', - order=1, - api_name='f1', - fieldset=fieldset, - ) - rule = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='10', - api_name='r1', - ) - rule.fields.add(field) + field = fieldset.fields.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() + fieldset_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', @@ -130,12 +117,22 @@ def test_create_fieldset__all_fields__ok(api_client, mocker): assert response.data['usage'] == [] assert len(response.data['fields']) == 1 - assert response.data['fields'][0]['name'] == 'Field 1' - assert response.data['fields'][0]['api_name'] == 'f1' + assert response.data['fields'][0]['name'] == field.name + assert response.data['fields'][0]['api_name'] == field.api_name + + assert len(response.data['rulesets']) == 1 + ruleset_data = response.data['rulesets'][0] + assert ruleset_data['api_name'] == ruleset.api_name - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['type'] == FieldSetRuleType.SUM_EQUAL - assert response.data['rules'][0]['api_name'] == 'r1' + assert len(ruleset_data['groups_or']) == 1 + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name + + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + assert group_and_data['operator'] == FieldSetRuleOperator.SUM_EQUAL + assert group_and_data['value'] == '10' fieldset_service_init_mock.assert_called_once_with( user=user, @@ -149,312 +146,14 @@ def test_create_fieldset__all_fields__ok(api_client, mocker): layout=data['layout'], label_position=data['label_position'], api_name=data['api_name'], - rules=data['rules'], - fields=data['fields'], - ) - - -def test_create_fieldset__min_data__ok(api_client, mocker): - - """Create fieldset with minimal request data""" - - # arrange - account = create_test_account() - user = create_test_owner(account=account) - template = create_test_template( - user=user, - tasks_count=1, - ) - data = { - 'name': 'Minimal Fieldset', - } - - fieldset_service_init_mock = mocker.patch.object( - FieldSetTemplateService, - attribute='__init__', - return_value=None, - ) - fieldset = FieldsetTemplate.objects.create( - account=account, - template=template, - name='Minimal Fieldset', - ) - fieldset_service_create_mock = mocker.patch( - 'src.processes.views.fieldset.FieldSetTemplateService.' - 'create_shared_fieldset', - return_value=fieldset, + rulesets=mocker.ANY, + fields=mocker.ANY, ) - api_client.token_authenticate(user=user) - - # act - response = api_client.post('/fieldsets', data=data) - - # assert - assert response.status_code == 201 - fieldset_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - ) - fieldset_service_create_mock.assert_called_once_with( - name='Minimal Fieldset', - rules=[], - fields=[], - ) +def test_create__unauthenticated__unauthorized(api_client, mocker): -def test_create_fieldset__set_api_name__ok(api_client, mocker): - - # arrange - account = create_test_account() - user = create_test_owner(account=account) - template = create_test_template( - user=user, - tasks_count=1, - ) - data = { - 'name': 'Minimal Fieldset', - 'api_name': 'fs1', - } - - fieldset_service_init_mock = mocker.patch.object( - FieldSetTemplateService, - attribute='__init__', - return_value=None, - ) - fieldset = FieldsetTemplate.objects.create( - account=account, - template=template, - name='Minimal Fieldset', - ) - fieldset_service_create_mock = mocker.patch( - 'src.processes.views.fieldset.FieldSetTemplateService.' - 'create_shared_fieldset', - return_value=fieldset, - ) - - api_client.token_authenticate(user=user) - - # act - response = api_client.post('/fieldsets', data=data) - - # assert - assert response.status_code == 201 - assert response.data['title'] == '' - fieldset_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - ) - fieldset_service_create_mock.assert_called_once_with( - name=data['name'], - api_name=data['api_name'], - rules=[], - fields=[], - ) - - -def test_create_fieldset__rule_with_one_field__ok(api_client, mocker): - - """ - Create fieldset with fields in rule request and check fields in response - """ - - # arrange - account = create_test_account() - user = create_test_owner(account=account) - template = create_test_template( - user=user, - tasks_count=1, - ) - template.tasks.first() - field_api_name = 'f1' - data = { - 'name': 'All Fields Fieldset', - 'fields': [ - { - 'name': 'Field 1', - 'type': FieldType.STRING, - 'order': 2, - 'api_name': field_api_name, - }, - { - 'name': 'Field 2', - 'type': FieldType.URL, - 'order': 1, - 'api_name': 'f2', - }, - ], - 'rules': [ - { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '10', - 'fields': [field_api_name], - }, - ], - } - - fieldset_service_init_mock = mocker.patch.object( - FieldSetTemplateService, - attribute='__init__', - return_value=None, - ) - - fieldset = FieldsetTemplate.objects.create( - account=account, - template=template, - name=data['name'], - ) - field = FieldTemplate.objects.create( - account=account, - template=template, - api_name=field_api_name, - fieldset=fieldset, - ) - rule = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='10', - ) - rule.fields.add(field) - - fieldset_service_create_mock = mocker.patch( - 'src.processes.views.fieldset.FieldSetTemplateService.' - 'create_shared_fieldset', - return_value=fieldset, - ) - - api_client.token_authenticate(user=user) - - # act - response = api_client.post('/fieldsets', data=data) - - # assert - assert response.status_code == 201 - - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['fields'] == [field_api_name] - - fieldset_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - ) - fieldset_service_create_mock.assert_called_once_with( - name=data['name'], - rules=data['rules'], - fields=data['fields'], - ) - - -def test_create_fieldset__rule_with_two_fields__ok(api_client, mocker): - - """ - Create fieldset with fields in rule request and check fields in response - """ - - # arrange - account = create_test_account() - user = create_test_owner(account=account) - template = create_test_template( - user=user, - tasks_count=1, - ) - field_1_api_name = 'f1' - field_2_api_name = 'f2' - data = { - 'name': 'All Fields Fieldset', - 'fields': [ - { - 'name': 'Field 1', - 'type': FieldType.STRING, - 'order': 2, - 'api_name': field_1_api_name, - }, - { - 'name': 'Field 2', - 'type': FieldType.URL, - 'order': 1, - 'api_name': field_2_api_name, - }, - ], - 'rules': [ - { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '10', - 'fields': [field_2_api_name, field_1_api_name], - }, - ], - } - - fieldset_service_init_mock = mocker.patch.object( - FieldSetTemplateService, - attribute='__init__', - return_value=None, - ) - - fieldset = FieldsetTemplate.objects.create( - account=account, - template=template, - name=data['name'], - ) - field_1 = FieldTemplate.objects.create( - account=account, - template=template, - api_name=field_1_api_name, - fieldset=fieldset, - ) - field_2 = FieldTemplate.objects.create( - account=account, - template=template, - api_name=field_2_api_name, - fieldset=fieldset, - ) - rule = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='10', - ) - rule.fields.set([field_1, field_2]) - - fieldset_service_create_mock = mocker.patch( - 'src.processes.views.fieldset.FieldSetTemplateService.' - 'create_shared_fieldset', - return_value=fieldset, - ) - - api_client.token_authenticate(user=user) - - # act - response = api_client.post('/fieldsets', data=data) - - # assert - assert response.status_code == 201 - - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['fields'] == [ - field_1_api_name, - field_2_api_name, - ] - - fieldset_service_init_mock.assert_called_once_with( - user=user, - is_superuser=False, - auth_type=AuthTokenType.USER, - ) - fieldset_service_create_mock.assert_called_once_with( - name=data['name'], - rules=data['rules'], - fields=data['fields'], - ) - - -def test_create_fieldset__unauthenticated__unauthorized(api_client, mocker): - - """Unauthenticated request returns 401""" + """ Unauthenticated request returns 401 """ # arrange data = { @@ -480,9 +179,9 @@ def test_create_fieldset__unauthenticated__unauthorized(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__expired_sub__permission_denied(api_client, mocker): +def test_create__expired_sub__permission_denied(api_client, mocker): - """Expired subscription returns 403""" + """ Expired subscription returns 403 """ # arrange account = create_test_account( @@ -516,7 +215,7 @@ def test_create_fieldset__expired_sub__permission_denied(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__billing_plan__permission_denied(api_client, mocker): +def test_create__billing_plan__permission_denied(api_client, mocker): """ Billing plan permission denied returns 403 """ @@ -547,7 +246,7 @@ def test_create_fieldset__billing_plan__permission_denied(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__users_limit__permission_denied(api_client, mocker): +def test_create__users_limit__permission_denied(api_client, mocker): """ Users overlimited returns 403 """ @@ -589,7 +288,7 @@ def test_create_fieldset__users_limit__permission_denied(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__non_admin__permission_denied(api_client, mocker): +def test_create__non_admin__permission_denied(api_client, mocker): """ Non-admin non-owner user returns 403 """ @@ -621,7 +320,7 @@ def test_create_fieldset__non_admin__permission_denied(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__admin__ok(api_client, mocker): +def test_create__admin__ok(api_client, mocker): """ Admin (non-owner) user can create fieldset """ @@ -629,16 +328,11 @@ def test_create_fieldset__admin__ok(api_client, mocker): account = create_test_account() create_test_owner(account=account) user = create_test_admin(account=account) - template = create_test_template( - user=user, - tasks_count=1, - ) data = { 'name': 'New Fieldset', } - fieldset = FieldsetTemplate.objects.create( + fieldset = create_test_shared_fieldset( account=account, - template=template, name=data['name'], ) fieldset_service_init_mock = mocker.patch.object( @@ -666,12 +360,12 @@ def test_create_fieldset__admin__ok(api_client, mocker): ) fieldset_service_create_mock.assert_called_once_with( name=data['name'], - rules=[], + rulesets=[], fields=[], ) -def test_create_fieldset__blank_name__validation_error(api_client, mocker): +def test_create__blank_name__validation_error(api_client, mocker): """ Invalid name field returns validation error """ @@ -705,7 +399,7 @@ def test_create_fieldset__blank_name__validation_error(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__invalid_layout__validation_error(api_client, mocker): +def test_create__invalid_layout__validation_error(api_client, mocker): """ Invalid layout field returns validation error """ @@ -740,7 +434,7 @@ def test_create_fieldset__invalid_layout__validation_error(api_client, mocker): fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__invalid_label_position__validation_error( +def test_create__invalid_label_position__validation_error( api_client, mocker, ): @@ -778,7 +472,7 @@ def test_create_fieldset__invalid_label_position__validation_error( fieldset_service_create_mock.assert_not_called() -def test_create_fieldset__service_exception__validation_error( +def test_create__service_exception__validation_error( api_client, mocker, ): @@ -818,6 +512,410 @@ def test_create_fieldset__service_exception__validation_error( ) fieldset_service_create_mock.assert_called_once_with( name='Test Fieldset', - rules=[], + rulesets=[], fields=[], ) + + +def test_create__rule_invalid_operator__validation_error(api_client, mocker): + + """ + Create fieldset with operator 'equal' (FieldRuleOperator) which is + not valid for FieldSetRuleOperator and verify 400 is returned. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Invalid Operator Fieldset', + 'rulesets': [ + { + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': 'equal', + 'value': '100', + }, + ], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = '"equal" is not a valid choice.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__rule_missing_operator__validation_error(api_client, mocker): + + """ + Create fieldset with a group_and that is missing the required 'operator' + field and verify the request is rejected with a 400 status code. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Missing Operator Fieldset', + 'rulesets': [ + { + 'groups_or': [ + { + 'groups_and': [ + { + 'value': '100', + }, + ], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'This field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__rule_missing_group_or__validation_error(api_client, mocker): + + """ + Create fieldset with a rule that is missing the required 'groups_or' field + and verify the request is rejected with a 400 status code. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Missing GroupOr Fieldset', + 'rulesets': [ + { + 'api_name': 'ruleset-1', + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Groups_or: this field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__field_rule_missing_type__validation_error(api_client, mocker): + + """ + Create fieldset with a field rule that is missing the required 'type' field + and verify the request is rejected with a 400 status code. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Field Rule Missing Type Fieldset', + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.TEXT, + 'order': 1, + 'api_name': 'f-1', + 'rulesets': [ + { + 'groups_or': [], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Type: this field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__field_rule_invalid_type__validation_error(api_client, mocker): + + """ + Create fieldset with a field rule using type 'sum_equal' + and verify 400 is returned. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Field Rule Invalid Type Fieldset', + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.TEXT, + 'order': 1, + 'api_name': 'f-1', + 'rulesets': [ + { + 'type': 'sum_equal', + 'groups_or': [], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Type: "sum_equal" is not a valid choice.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__field_rule_invalid_op__validation_error(api_client, mocker): + + """ + Create fieldset with a field rule using operator 'sum_equal' which + belongs to FieldSetRuleOperator (not FieldRuleOperator) and verify + 400 is returned. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Field Rule Invalid Operator Fieldset', + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.TEXT, + 'order': 1, + 'api_name': 'f-1', + 'rulesets': [ + { + 'type': FieldRuleType.SHOW, + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': ( + FieldSetRuleOperator.SUM_EQUAL + ), + 'value': '10', + }, + ], + }, + ], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Field: this field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__field_rule_missing_op__validation_error(api_client, mocker): + + """ + Create fieldset with a field rule group_and that is missing the required + 'operator' field and verify the request is rejected with a 400 status code. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Field Rule Missing Operator Fieldset', + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.TEXT, + 'order': 1, + 'api_name': 'f-1', + 'rulesets': [ + { + 'type': FieldRuleType.SHOW, + 'groups_or': [ + { + 'groups_and': [ + { + 'value': 'yes', + }, + ], + }, + ], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Field: this field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__field_rule_grp_and_field_missing__err(api_client, mocker): + + """ + Create fieldset with a field rule group_and that is missing the + required 'field' key and verify 400 is returned. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Field Rule Missing Field Key Fieldset', + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.TEXT, + 'order': 1, + 'api_name': 'f-1', + 'rulesets': [ + { + 'type': FieldRuleType.SHOW, + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldRuleOperator.EQUAL, + 'value': 'yes', + }, + ], + }, + ], + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Field: this field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() + + +def test_create__field_rule_missing_grp_or__err(api_client, mocker): + + """ + Create fieldset with a field rule that is missing 'groups_or' + and verify the request is rejected with 400. + """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + data = { + 'name': 'Field Rule Missing GroupOr Fieldset', + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.TEXT, + 'order': 1, + 'api_name': 'f-1', + 'rulesets': [ + { + 'type': FieldRuleType.SHOW, + }, + ], + }, + ], + } + create_shared_fieldset_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.' + 'create_shared_fieldset', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.post('/fieldsets', data=data) + + # assert + assert response.status_code == 400 + message = 'Groups_or: this field is required.' + assert response.data['message'] == message + create_shared_fieldset_mock.assert_not_called() diff --git a/backend/src/processes/tests/test_views/test_fieldsets/test_list.py b/backend/src/processes/tests/test_views/test_fieldsets/test_list.py index 470c35e43..95f885e94 100644 --- a/backend/src/processes/tests/test_views/test_fieldsets/test_list.py +++ b/backend/src/processes/tests/test_views/test_fieldsets/test_list.py @@ -7,7 +7,14 @@ from src.accounts.enums import BillingPlanType from src.accounts.messages import MSG_A_0035, MSG_A_0037, MSG_A_0041 from src.processes.enums import ( - FieldSetRuleType, + FieldSetRuleOperator, + FieldRuleType, + FieldRuleOperator, +) +from src.processes.models.templates.fields import ( + FieldTemplateRuleSet, + FieldTemplateRuleGroupOr, + FieldTemplateRuleGroupAnd, ) from src.processes.tests.fixtures import ( create_test_account, @@ -25,22 +32,47 @@ def test_list_fieldsets__all_data__ok(api_client): - """List fieldsets returning all fields including title and order""" + """ List fieldsets returning all fields including title and order """ # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' fieldset = create_test_shared_fieldset( account=account, title='Fieldset Title', order=3, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) field = fieldset.fields.get() - rule = fieldset.rules.get() + rule = fieldset.rulesets.first() + group_or = rule.groups_or.first() + group_and = group_or.groups_and.first() + + field_rule_type = FieldRuleType.SHOW + field_ruleset = FieldTemplateRuleSet.objects.create( + field=field, + account=account, + api_name=f'{field.api_name}-ruleset-1', + type=field_rule_type, + order=0, + ) + field_group_or = FieldTemplateRuleGroupOr.objects.create( + field_rule=field_ruleset, + account=account, + api_name=f'{field.api_name}-group-or-1', + ) + field_rule_value = 'some value' + field_group_and = FieldTemplateRuleGroupAnd.objects.create( + group_or=field_group_or, + account=account, + api_name=f'{field.api_name}-group-and-1', + operator=FieldRuleOperator.EQUAL, + value=field_rule_value, + field=field.api_name, + ) api_client.token_authenticate(user=user) @@ -61,23 +93,53 @@ def test_list_fieldsets__all_data__ok(api_client): assert fieldset_data['label_position'] == fieldset.label_position assert fieldset_data['usage'] == [] - assert len(fieldset_data['rules']) == 1 - rules_data = fieldset_data['rules'] - assert rules_data[0]['type'] == rule_type - assert rules_data[0]['value'] == rule_value - assert rules_data[0]['api_name'] == rule.api_name + assert len(fieldset_data['rulesets']) == 1 + rule_data = fieldset_data['rulesets'][0] + assert rule_data['api_name'] == rule.api_name + assert rule_data['message'] is None + assert rule_data['order'] == 0 + assert rule_data['fields'] == [] + + assert len(rule_data['groups_or']) == 1 + group_or_data = rule_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name + + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + assert group_and_data['operator'] == rule_operator + assert group_and_data['value'] == rule_value assert len(fieldset_data['fields']) == 1 - fields_data = fieldset_data['fields'] - assert fields_data[0]['name'] == field.name - assert fields_data[0]['type'] == field.type - assert fields_data[0]['api_name'] == field.api_name - assert fields_data[0]['description'] == '' - assert fields_data[0]['is_required'] is False - assert fields_data[0]['is_hidden'] is False - assert fields_data[0]['default'] == '' - assert 'dataset' not in fields_data[0] - assert 'selections' not in fields_data[0] + field_data = fieldset_data['fields'][0] + assert field_data['name'] == field.name + assert field_data['type'] == field.type + assert field_data['api_name'] == field.api_name + assert field_data['description'] == '' + assert field_data['is_required'] is False + assert field_data['is_hidden'] is False + assert field_data['default'] == '' + assert field_data['order'] == field.order + assert 'dataset' not in field_data + assert 'selections' not in field_data + + assert len(field_data['rulesets']) == 1 + field_rule_data = field_data['rulesets'][0] + assert field_rule_data['api_name'] == field_ruleset.api_name + assert field_rule_data['type'] == field_rule_type + assert field_rule_data['message'] is None + assert field_rule_data['order'] == 0 + + assert len(field_rule_data['groups_or']) == 1 + field_group_or_data = field_rule_data['groups_or'][0] + assert field_group_or_data['api_name'] == field_group_or.api_name + + assert len(field_group_or_data['groups_and']) == 1 + field_group_and_data = field_group_or_data['groups_and'][0] + assert field_group_and_data['api_name'] == field_group_and.api_name + assert field_group_and_data['field'] == field.api_name + assert field_group_and_data['operator'] == FieldRuleOperator.EQUAL + assert field_group_and_data['value'] == field_rule_value def test_list_fieldsets__used_in_tasks__return_usage( @@ -87,13 +149,13 @@ def test_list_fieldsets__used_in_tasks__return_usage( # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' shared_fieldset = create_test_shared_fieldset( account=account, title='Fieldset Title', order=3, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) template_1 = create_test_template( @@ -142,13 +204,13 @@ def test_list_fieldsets__used_in_kickoff_and_tasks__return_usage( # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' shared_fieldset = create_test_shared_fieldset( account=account, title='Fieldset Title', order=3, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) template_1 = create_test_template( @@ -197,13 +259,13 @@ def test_list_fieldsets__used_twice_in_one_template__return_usage( # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' shared_fieldset = create_test_shared_fieldset( account=account, title='Fieldset Title', order=3, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) template = create_test_template( @@ -239,22 +301,24 @@ def test_list_fieldsets__used_twice_in_one_template__return_usage( assert shared_fieldset_data['usage'][0]['name'] == template.name -def test_list_fieldsets__shared_fieldset_has_rules_and_fields__ok(api_client): +def test_list_fieldsets__shared_fieldset_has_rules_and_fields__ok( + api_client, +): - """List shared fieldsets returns rules and fields""" + """ List shared fieldsets returns rules and fields """ # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' fieldset = create_test_shared_fieldset( account=account, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) field = fieldset.fields.get() - rule = fieldset.rules.get() + rule = fieldset.rulesets.first() api_client.token_authenticate(user=user) @@ -265,14 +329,14 @@ def test_list_fieldsets__shared_fieldset_has_rules_and_fields__ok(api_client): assert response.status_code == 200 data = response.data[0] assert data['id'] == fieldset.id - assert len(data['rules']) == 1 - assert data['rules'][0]['api_name'] == rule.api_name + assert len(data['rulesets']) == 1 + assert data['rulesets'][0]['api_name'] == rule.api_name assert len(data['fields']) == 1 assert data['fields'][0]['api_name'] == field.api_name def test_list_fieldsets__pagination__ok(api_client): - """Paginated list returns correct count and slice""" + """ Paginated list returns correct count and slice """ # arrange account = create_test_account() @@ -308,7 +372,7 @@ def test_list_fieldsets__pagination__ok(api_client): def test_list_fieldsets__different_accounts__ok(api_client): - """List fieldsets filtered by account — other accounts excluded""" + """ List fieldsets filtered by account — other accounts excluded """ # arrange account_1 = create_test_account(name='Account 1') @@ -338,20 +402,20 @@ def test_list_fieldsets__different_accounts__ok(api_client): def test_list_fieldsets__rule_with_fields__ok(api_client): - """List fieldsets returning rules mapping to fields""" + """ List fieldsets returning rules mapping to fields """ # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' fieldset = create_test_shared_fieldset( account=account, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) field = fieldset.fields.get() - rule = fieldset.rules.get() + rule = fieldset.rulesets.first() rule.fields.add(field) api_client.token_authenticate(user=user) @@ -364,103 +428,13 @@ def test_list_fieldsets__rule_with_fields__ok(api_client): assert len(response.data) == 1 fieldset_data = response.data[0] - assert len(fieldset_data['rules']) == 1 - rules_data = fieldset_data['rules'] + assert len(fieldset_data['rulesets']) == 1 + rules_data = fieldset_data['rulesets'] assert rules_data[0]['fields'] == [field.api_name] -def test_list_fieldsets__unauthenticated__unauthorized(api_client): - """Unauthenticated request returns 401""" - - # act - response = api_client.get('/fieldsets') - - # assert - assert response.status_code == 401 - - -def test_list_fieldsets__expired_sub__permission_denied(api_client): - """Expired subscription returns 403""" - - # arrange - account = create_test_account( - plan=BillingPlanType.PREMIUM, - plan_expiration=timezone.now() - timedelta(days=1), - ) - user = create_test_owner(account=account) - - api_client.token_authenticate(user=user) - - # act - response = api_client.get('/fieldsets') - - # assert - assert response.status_code == 403 - assert response.data['detail'] == MSG_A_0035 - - -def test_list_fieldsets__billing_plan__permission_denied(api_client): - """Billing plan permission denied returns 403""" - - # arrange - account = create_test_account(plan=None) - user = create_test_owner(account=account) - - api_client.token_authenticate(user=user) - - # act - response = api_client.get('/fieldsets') - - # assert - assert response.status_code == 403 - assert response.data['detail'] == MSG_A_0041 - - -def test_list_fieldsets__users_overlimit__permission_denied(api_client): - """Users overlimited returns 403""" - - # arrange - account = create_test_account( - plan=BillingPlanType.PREMIUM, - max_users=1, - ) - user = create_test_owner(account=account) - create_test_not_admin( - account=account, - email='extra@pneumatic.app', - ) - account.active_users = 2 - account.save() - - api_client.token_authenticate(user=user) - - # act - response = api_client.get('/fieldsets') - - # assert - assert response.status_code == 403 - assert response.data['detail'] == MSG_A_0037 - - -def test_list_fieldsets__non_admin__permission_denied(api_client): - """Non-admin non-owner user returns 403""" - - # arrange - account = create_test_account() - create_test_owner(account=account) - user = create_test_not_admin(account=account) - - api_client.token_authenticate(user=user) - - # act - response = api_client.get('/fieldsets') - - # assert - assert response.status_code == 403 - - def test_list_fieldsets__admin__ok(api_client): - """Admin (non-owner) user can list fieldsets""" + """ Admin (non-owner) user can list fieldsets """ # arrange account = create_test_account() @@ -829,6 +803,8 @@ def test_list_fieldsets__soft_deleted__ok(api_client): def test_list_fieldsets__not_shared__empty_list(api_client): + """ Non-shared fieldset is excluded from the list """ + # arrange account = create_test_account() user = create_test_owner(account=account) @@ -846,3 +822,93 @@ def test_list_fieldsets__not_shared__empty_list(api_client): # assert assert response.status_code == 200 assert len(response.data) == 0 + + +def test_list_fieldsets__unauthenticated__unauthorized(api_client): + """ Unauthenticated request returns 401 """ + + # act + response = api_client.get('/fieldsets') + + # assert + assert response.status_code == 401 + + +def test_list_fieldsets__expired_sub__permission_denied(api_client): + """ Expired subscription returns 403 """ + + # arrange + account = create_test_account( + plan=BillingPlanType.PREMIUM, + plan_expiration=timezone.now() - timedelta(days=1), + ) + user = create_test_owner(account=account) + + api_client.token_authenticate(user=user) + + # act + response = api_client.get('/fieldsets') + + # assert + assert response.status_code == 403 + assert response.data['detail'] == MSG_A_0035 + + +def test_list_fieldsets__billing_plan__permission_denied(api_client): + """ Billing plan permission denied returns 403 """ + + # arrange + account = create_test_account(plan=None) + user = create_test_owner(account=account) + + api_client.token_authenticate(user=user) + + # act + response = api_client.get('/fieldsets') + + # assert + assert response.status_code == 403 + assert response.data['detail'] == MSG_A_0041 + + +def test_list_fieldsets__users_overlimit__permission_denied(api_client): + """ Users overlimited returns 403 """ + + # arrange + account = create_test_account( + plan=BillingPlanType.PREMIUM, + max_users=1, + ) + user = create_test_owner(account=account) + create_test_not_admin( + account=account, + email='extra@pneumatic.app', + ) + account.active_users = 2 + account.save() + + api_client.token_authenticate(user=user) + + # act + response = api_client.get('/fieldsets') + + # assert + assert response.status_code == 403 + assert response.data['detail'] == MSG_A_0037 + + +def test_list_fieldsets__non_admin__permission_denied(api_client): + """ Non-admin non-owner user returns 403 """ + + # arrange + account = create_test_account() + create_test_owner(account=account) + user = create_test_not_admin(account=account) + + api_client.token_authenticate(user=user) + + # act + response = api_client.get('/fieldsets') + + # assert + assert response.status_code == 403 diff --git a/backend/src/processes/tests/test_views/test_fieldsets/test_partial_update.py b/backend/src/processes/tests/test_views/test_fieldsets/test_partial_update.py index 5d98a4141..8a16200b6 100644 --- a/backend/src/processes/tests/test_views/test_fieldsets/test_partial_update.py +++ b/backend/src/processes/tests/test_views/test_fieldsets/test_partial_update.py @@ -10,10 +10,11 @@ from src.processes.enums import ( FieldSetLayout, LabelPosition, - FieldSetRuleType, FieldType, + FieldRuleType, + FieldRuleOperator, + FieldSetRuleOperator, ) -from src.processes.models.templates.fieldset import FieldsetTemplateRule from src.processes.models.templates.fields import FieldTemplate from src.processes.services.fieldsets.fieldset import ( FieldSetTemplateService, @@ -30,9 +31,12 @@ pytestmark = pytest.mark.django_db -def test_partial_update__all_fields__ok(api_client, mocker): +def test_partial_update__fieldset_data__ok(api_client, mocker): - """ Partial update with full request data """ + """ + Partial update with all top-level fieldset fields returns 200 + and correct response data + """ # arrange account = create_test_account() @@ -54,12 +58,20 @@ def test_partial_update__all_fields__ok(api_client, mocker): 'api_name': field_api_name, }, ], - 'rules': [ + 'rulesets': [ { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '10', 'api_name': rule_api_name, 'fields': [field_api_name], + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '10', + }, + ], + }, + ], }, ], } @@ -70,11 +82,13 @@ def test_partial_update__all_fields__ok(api_client, mocker): label_position=data['label_position'], layout=data['layout'], api_name=data['api_name'], - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + rule_value='10', ) field = fieldset.fields.first() - rule = fieldset.rules.first() + rule = fieldset.rulesets.first() rule.fields.add(field) + group_and = rule.groups_or.first().groups_and.first() fieldset_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', @@ -84,7 +98,6 @@ def test_partial_update__all_fields__ok(api_client, mocker): 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', return_value=fieldset, ) - api_client.token_authenticate(user=user) # act @@ -96,22 +109,26 @@ def test_partial_update__all_fields__ok(api_client, mocker): # assert assert response.status_code == 200 assert response.data['id'] == fieldset.id - assert response.data['name'] == data['name'] - assert response.data['description'] == data['description'] - assert response.data['label_position'] == data['label_position'] - assert response.data['layout'] == data['layout'] - assert response.data['api_name'] == data['api_name'] + assert response.data['name'] == fieldset.name + assert response.data['title'] == fieldset.title + assert response.data['order'] == fieldset.order + assert response.data['description'] == fieldset.description + assert response.data['label_position'] == fieldset.label_position + assert response.data['layout'] == fieldset.layout + assert response.data['api_name'] == fieldset.api_name assert response.data['usage'] == [] assert len(response.data['fields']) == 1 assert response.data['fields'][0]['name'] == field.name assert response.data['fields'][0]['api_name'] == field.api_name - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['type'] == rule.type - assert response.data['rules'][0]['value'] == rule.value - assert response.data['rules'][0]['api_name'] == rule.api_name - assert response.data['rules'][0]['fields'] == [field.api_name] + assert len(response.data['rulesets']) == 1 + assert response.data['rulesets'][0]['api_name'] == rule.api_name + assert response.data['rulesets'][0]['fields'] == [field.api_name] + assert ( + response.data['rulesets'][0]['groups_or'][0]['groups_and'][0]['value'] + == group_and.value + ) fieldset_service_init_mock.assert_called_once_with( user=user, instance=fieldset, @@ -124,25 +141,53 @@ def test_partial_update__all_fields__ok(api_client, mocker): description='Updated description', layout=FieldSetLayout.HORIZONTAL, label_position=LabelPosition.LEFT, - rules=data['rules'], - fields=data['fields'], + rulesets=mocker.ANY, + fields=mocker.ANY, ) -def test_partial_update__name__ok(api_client, mocker): +@pytest.mark.parametrize( + 'operator', + ( + FieldSetRuleOperator.SUM_EQUAL, + FieldSetRuleOperator.SUM_LESS_THAN, + FieldSetRuleOperator.SUM_GREATER_THAN, + ), +) +def test_partial_update__rules_operator__ok( + api_client, + operator, + mocker, +): - """ Partial update with minimal request data """ + """ Valid fieldset rules operator is accepted and passed to service """ # arrange account = create_test_account() user = create_test_owner(account=account) - fieldset = create_test_shared_fieldset( - account=account, - ) + fieldset = create_test_shared_fieldset(account=account) data = { - 'name': 'Updated Name', + 'rulesets': [ + { + 'api_name': 'r-1', + 'order': 1, + 'fields': ['field-1', 'field-2'], + 'groups_or': [ + { + 'api_name': 'g-or-1', + 'groups_and': [ + { + 'api_name': 'g-and-1', + 'operator': operator, + 'value': '100', + }, + ], + }, + ], + }, + ], } - fieldset_service_init_mock = mocker.patch.object( + field_set_template_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', return_value=None, @@ -162,14 +207,15 @@ def test_partial_update__name__ok(api_client, mocker): # assert assert response.status_code == 200 assert response.data['id'] == fieldset.id - fieldset_service_init_mock.assert_called_once_with( + assert response.data['api_name'] == fieldset.api_name + field_set_template_service_init_mock.assert_called_once_with( user=user, instance=fieldset, is_superuser=False, auth_type=AuthTokenType.USER, ) fieldset_partial_update_mock.assert_called_once_with( - name=data['name'], + rulesets=mocker.ANY, ) @@ -231,63 +277,67 @@ def test_partial_update_fieldset_is_used__return_usage(api_client, mocker): ) -def test_partial_update__with_rule_fields__ok(api_client, mocker): +@pytest.mark.parametrize( + 'operator', + ( + FieldRuleOperator.EQUAL, + FieldRuleOperator.GREATER_THAN, + FieldRuleOperator.LESS_THAN, + ), +) +def test_partial_update__fields_operator__ok( + api_client, + operator, + mocker, +): - """ - Partial update with fields in rule request - and check fields in response - """ + """ Valid field rules operator is accepted and passed to service """ # arrange account = create_test_account() user = create_test_owner(account=account) - fieldset = create_test_shared_fieldset( - account=account, - ) - field_api_name = 'f1' + fieldset = create_test_shared_fieldset(account=account) + field = fieldset.fields.first() data = { - 'name': 'Updated Fieldset', 'fields': [ { 'name': 'Field 1', - 'type': FieldType.STRING, - 'order': 1, - 'api_name': field_api_name, - }, - ], - 'rules': [ - { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '10', - 'api_name': 'r1', - 'fields': [field_api_name], + 'type': FieldType.NUMBER, + 'order': 2, + 'api_name': 'field-1', + 'is_hidden': True, + 'rulesets': [ + { + 'api_name': 'r-1', + 'type': FieldRuleType.VALIDATOR, + 'order': 1, + 'groups_or': [ + { + 'api_name': 'g-or-1', + 'groups_and': [ + { + 'api_name': 'g-and-1', + 'field': field.api_name, + 'operator': operator, + 'value': 'apple', + }, + ], + }, + ], + }, + ], }, ], } - - # mock FieldSetTemplateService - fieldset_service_init_mock = mocker.patch.object( + field_set_template_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', return_value=None, ) - - # Pre-add the field to the fieldset for the mock response verification - field = fieldset.fields.first() - rule = FieldsetTemplateRule.objects.create( - account=account, - fieldset=fieldset, - type=FieldSetRuleType.SUM_EQUAL, - value='val', - api_name='r1', - ) - rule.fields.add(field) - - fieldset_partial_update_mock = mocker.patch( + partial_update_mock = mocker.patch( 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', return_value=fieldset, ) - api_client.token_authenticate(user=user) # act @@ -299,55 +349,217 @@ def test_partial_update__with_rule_fields__ok(api_client, mocker): # assert assert response.status_code == 200 assert response.data['id'] == fieldset.id + assert response.data['api_name'] == fieldset.api_name + field_set_template_service_init_mock.assert_called_once_with( + user=user, + instance=fieldset, + is_superuser=False, + auth_type=AuthTokenType.USER, + ) + partial_update_mock.assert_called_once() - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['api_name'] == 'r1' - assert response.data['rules'][0]['fields'] == [field.api_name] - fieldset_service_init_mock.assert_called_once_with( +def test_partial_update__response_rules_data__ok(api_client, mocker): + + """ Verify full rules response: ruleset → group_or → group_and """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + fieldset = create_test_shared_fieldset( + account=account, + rule_value='100', + rule_message='Error message', + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + ) + field = fieldset.fields.first() + ruleset = fieldset.rulesets.first() + ruleset.fields.add(field) + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() + data = { + 'name': fieldset.name, + } + field_set_template_service_init_mock = mocker.patch.object( + FieldSetTemplateService, + attribute='__init__', + return_value=None, + ) + partial_update_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', + return_value=fieldset, + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.patch( + f'/fieldsets/{fieldset.id}', + data=data, + ) + + # assert + assert response.status_code == 200 + assert len(response.data['rulesets']) == 1 + + rule_resp = response.data['rulesets'][0] + assert rule_resp['api_name'] == ruleset.api_name + assert rule_resp['message'] == ruleset.message + assert rule_resp['order'] == ruleset.order + assert rule_resp['fields'] == [field.api_name] + + assert len(rule_resp['groups_or']) == 1 + group_or_resp = rule_resp['groups_or'][0] + assert group_or_resp['api_name'] == group_or.api_name + + assert len(group_or_resp['groups_and']) == 1 + group_and_resp = group_or_resp['groups_and'][0] + assert group_and_resp['api_name'] == group_and.api_name + assert group_and_resp['operator'] == group_and.operator + assert group_and_resp['value'] == group_and.value + field_set_template_service_init_mock.assert_called_once_with( user=user, instance=fieldset, is_superuser=False, auth_type=AuthTokenType.USER, ) - fieldset_partial_update_mock.assert_called_once_with( - name='Updated Fieldset', - rules=data['rules'], - fields=data['fields'], + partial_update_mock.assert_called_once_with( + name=data['name'], ) -def test_partial_update__clear_fields__ok(api_client, mocker): +def test_partial_update__response_fields_data__ok(api_client, mocker): + + """ Verify full fields response: field → rules → group_or → group_and """ # arrange account = create_test_account() user = create_test_owner(account=account) fieldset = create_test_shared_fieldset( account=account, + field_rule_type=FieldRuleType.SHOW, + field_rule_operator=FieldRuleOperator.EQUAL, + field_rule_value='apple', + field_rule_message='Error message', ) + field = fieldset.fields.first() + field_ruleset = field.rulesets.first() + field_group_or = field_ruleset.groups_or.first() + field_group_and = field_group_or.groups_and.first() data = { - 'name': 'Updated Fieldset', - 'rules': [ + 'name': fieldset.name, + } + field_set_template_service_init_mock = mocker.patch.object( + FieldSetTemplateService, + attribute='__init__', + return_value=None, + ) + partial_update_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', + return_value=fieldset, + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.patch( + f'/fieldsets/{fieldset.id}', + data=data, + ) + + # assert + assert response.status_code == 200 + assert len(response.data['fields']) == 1 + + field_resp = response.data['fields'][0] + assert field_resp['api_name'] == field.api_name + assert field_resp['name'] == field.name + assert field_resp['type'] == field.type + + assert len(field_resp['rulesets']) == 1 + rule_resp = field_resp['rulesets'][0] + assert rule_resp['api_name'] == field_ruleset.api_name + assert rule_resp['message'] == field_ruleset.message + assert rule_resp['order'] == field_ruleset.order + + assert len(rule_resp['groups_or']) == 1 + group_or_resp = rule_resp['groups_or'][0] + assert group_or_resp['api_name'] == field_group_or.api_name + + assert len(group_or_resp['groups_and']) == 1 + group_and_resp = group_or_resp['groups_and'][0] + assert group_and_resp['api_name'] == field_group_and.api_name + assert group_and_resp['field'] == field.api_name + assert group_and_resp['operator'] == field_group_and.operator + assert group_and_resp['value'] == field_group_and.value + field_set_template_service_init_mock.assert_called_once_with( + user=user, + instance=fieldset, + is_superuser=False, + auth_type=AuthTokenType.USER, + ) + partial_update_mock.assert_called_once_with( + name=data['name'], + ) + + +@pytest.mark.parametrize( + 'rule_type', + ( + FieldRuleType.SHOW, + FieldRuleType.VALIDATOR, + ), +) +def test_partial_update__fields_rules_type__ok( + api_client, + rule_type, + mocker, +): + + """ Valid fieldset fields->rules->type values are accepted """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + fieldset = create_test_shared_fieldset(account=account) + field = fieldset.fields.first() + data = { + 'fields': [ { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '10', - 'api_name': 'r1', - 'fields': [], + 'name': 'Field 1', + 'type': FieldType.NUMBER, + 'order': 1, + 'api_name': 'field-1', + 'rulesets': [ + { + 'api_name': 'r-1', + 'type': rule_type, + 'order': 1, + 'groups_or': [ + { + 'api_name': 'g-or-1', + 'groups_and': [ + { + 'api_name': 'g-and-1', + 'field': field.api_name, + 'operator': FieldRuleOperator.EQUAL, + 'value': 'apple', + }, + ], + }, + ], + }, + ], }, ], } - - # mock FieldSetTemplateService - fieldset_service_init_mock = mocker.patch.object( + field_set_template_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', return_value=None, ) - fieldset_partial_update_mock = mocker.patch( + partial_update_mock = mocker.patch( 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', return_value=fieldset, ) - api_client.token_authenticate(user=user) # act @@ -358,16 +570,13 @@ def test_partial_update__clear_fields__ok(api_client, mocker): # assert assert response.status_code == 200 - fieldset_service_init_mock.assert_called_once_with( + field_set_template_service_init_mock.assert_called_once_with( user=user, instance=fieldset, is_superuser=False, auth_type=AuthTokenType.USER, ) - fieldset_partial_update_mock.assert_called_once_with( - name='Updated Fieldset', - rules=data['rules'], - ) + partial_update_mock.assert_called_once() def test_partial_update__unauthenticated__unauthorized(api_client, mocker): @@ -382,7 +591,6 @@ def test_partial_update__unauthenticated__unauthorized(api_client, mocker): data = { 'name': 'Updated Fieldset', } - fieldset_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', @@ -391,6 +599,7 @@ def test_partial_update__unauthenticated__unauthorized(api_client, mocker): fieldset_partial_update_mock = mocker.patch( 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', ) + # act response = api_client.patch( f'/fieldsets/{fieldset.id}', @@ -590,8 +799,7 @@ def test_partial_update__invalid_name__validation_error(api_client, mocker): # assert assert response.status_code == 400 - message = 'This field may not be blank.' - assert response.data['message'] == message + assert response.data['message'] == 'This field may not be blank.' assert response.data['details']['name'] == 'name' fieldset_service_init_mock.assert_not_called() fieldset_partial_update_mock.assert_not_called() @@ -680,7 +888,10 @@ def test_partial_update__service_exception__validation_error( api_client, mocker, ): - """Service raises BaseServiceException returns validation error""" + + """ + Service raises BaseServiceException returns 400 validation error + """ # arrange account = create_test_account() @@ -735,9 +946,6 @@ def test_partial_update__not_existing_fieldset__not_found(api_client, mocker): data = { 'name': 'Updated Fieldset', } - - api_client.token_authenticate(user=user) - fieldset_service_init_mock = mocker.patch.object( FieldSetTemplateService, attribute='__init__', @@ -746,6 +954,8 @@ def test_partial_update__not_existing_fieldset__not_found(api_client, mocker): fieldset_partial_update_mock = mocker.patch( 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', ) + api_client.token_authenticate(user=user) + # act response = api_client.patch( f'/fieldsets/{nonexistent_id}', @@ -796,10 +1006,138 @@ def test_partial_update__not_shared__not_found(api_client, mocker): fieldset_partial_update_mock.assert_not_called() +def test_partial_update__invalid_rules_operator__validation_error( + api_client, + mocker, +): + + """ Invalid operator in fieldset rules->group_or->group_and returns 400 """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + fieldset = create_test_shared_fieldset(account=account) + data = { + 'rulesets': [ + { + 'api_name': 'r-1', + 'order': 1, + 'fields': [], + 'groups_or': [ + { + 'api_name': 'g-or-1', + 'groups_and': [ + { + 'api_name': 'g-and-1', + 'operator': 'invalid_operator', + 'value': '100', + }, + ], + }, + ], + }, + ], + } + fieldset_service_init_mock = mocker.patch.object( + FieldSetTemplateService, + attribute='__init__', + return_value=None, + ) + fieldset_partial_update_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.patch( + f'/fieldsets/{fieldset.id}', + data=data, + ) + + # assert + assert response.status_code == 400 + message = 'Operator: "invalid_operator" is not a valid choice.' + assert response.data['message'] == message + assert response.data['code'] == ErrorCode.VALIDATION_ERROR + assert response.data['details']['api_name'] == 'g-and-1' + assert response.data['details']['reason'] == message + fieldset_service_init_mock.assert_not_called() + fieldset_partial_update_mock.assert_not_called() + + +def test_partial_update__invalid_fields_rules_operator__validation_error( + api_client, + mocker, +): + + """ Invalid operator in fields->rules->group_or->group_and returns 400 """ + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + fieldset = create_test_shared_fieldset(account=account) + field = fieldset.fields.first() + data = { + 'fields': [ + { + 'name': 'Field 1', + 'type': FieldType.NUMBER, + 'order': 1, + 'api_name': 'field-1', + 'rulesets': [ + { + 'api_name': 'r-1', + 'type': FieldRuleType.SHOW, + 'order': 1, + 'groups_or': [ + { + 'api_name': 'g-or-1', + 'groups_and': [ + { + 'api_name': 'g-and-1', + 'field': field.api_name, + 'operator': 'invalid_operator', + 'value': 'apple', + }, + ], + }, + ], + }, + ], + }, + ], + } + fieldset_service_init_mock = mocker.patch.object( + FieldSetTemplateService, + attribute='__init__', + return_value=None, + ) + fieldset_partial_update_mock = mocker.patch( + 'src.processes.views.fieldset.FieldSetTemplateService.partial_update', + ) + api_client.token_authenticate(user=user) + + # act + response = api_client.patch( + f'/fieldsets/{fieldset.id}', + data=data, + ) + + # assert + assert response.status_code == 400 + message = 'Operator: "invalid_operator" is not a valid choice.' + assert response.data['message'] == message + assert response.data['code'] == ErrorCode.VALIDATION_ERROR + assert response.data['details']['api_name'] == 'g-and-1' + assert response.data['details']['reason'] == message + fieldset_service_init_mock.assert_not_called() + fieldset_partial_update_mock.assert_not_called() + + def test_partial_update__two_rules_same_type__ok(api_client, mocker): """ - Partial update with two rules of the same type: + Partial update with two rules of the same operator: one existing (with api_name) and one new (without api_name). Both rules reference the same fields. """ @@ -809,7 +1147,7 @@ def test_partial_update__two_rules_same_type__ok(api_client, mocker): user = create_test_owner(account=account) fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) field_1 = fieldset.fields.first() field_2 = FieldTemplate.objects.create( @@ -819,22 +1157,38 @@ def test_partial_update__two_rules_same_type__ok(api_client, mocker): api_name='f2', fieldset=fieldset, ) - existing_rule = fieldset.rules.first() - existing_rule.fields.add(field_1) - existing_rule.fields.add(field_2) + existing_ruleset = fieldset.rulesets.first() + existing_ruleset.fields.add(field_1) + existing_ruleset.fields.add(field_2) data = { - 'rules': [ + 'rulesets': [ { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '1', + 'api_name': existing_ruleset.api_name, 'fields': [field_1.api_name, field_2.api_name], - 'api_name': existing_rule.api_name, + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '1', + }, + ], + }, + ], }, { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '100', 'fields': [field_1.api_name, field_2.api_name], + 'groups_or': [ + { + 'groups_and': [ + { + 'operator': FieldSetRuleOperator.SUM_EQUAL, + 'value': '100', + }, + ], + }, + ], }, ], } @@ -866,5 +1220,5 @@ def test_partial_update__two_rules_same_type__ok(api_client, mocker): auth_type=AuthTokenType.USER, ) fieldset_partial_update_mock.assert_called_once_with( - rules=data['rules'], + rulesets=mocker.ANY, ) diff --git a/backend/src/processes/tests/test_views/test_fieldsets/test_retrieve.py b/backend/src/processes/tests/test_views/test_fieldsets/test_retrieve.py index 20e499752..1dfb2eb7f 100644 --- a/backend/src/processes/tests/test_views/test_fieldsets/test_retrieve.py +++ b/backend/src/processes/tests/test_views/test_fieldsets/test_retrieve.py @@ -5,7 +5,7 @@ from src.accounts.enums import BillingPlanType from src.accounts.messages import MSG_A_0035, MSG_A_0037, MSG_A_0041 -from src.processes.enums import FieldSetRuleType +from src.processes.enums import FieldSetRuleOperator from src.processes.tests.fixtures import ( create_test_account, create_test_shared_fieldset, @@ -22,16 +22,18 @@ def test_retrieve__fieldset_all_data__ok(api_client): # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' fieldset = create_test_shared_fieldset( account=account, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) field = fieldset.fields.get() - rule = fieldset.rules.get() - rule.fields.add(field) + ruleset = fieldset.rulesets.get() + ruleset.fields.add(field) + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() api_client.token_authenticate(user=user) # act @@ -49,11 +51,17 @@ def test_retrieve__fieldset_all_data__ok(api_client): assert response.data['label_position'] == fieldset.label_position assert response.data['usage'] == [] - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['type'] == rule.type - assert response.data['rules'][0]['value'] == rule.value - assert response.data['rules'][0]['api_name'] == rule.api_name - assert response.data['rules'][0]['fields'] == [field.api_name] + assert len(response.data['rulesets']) == 1 + rule_resp = response.data['rulesets'][0] + assert rule_resp['api_name'] == ruleset.api_name + assert rule_resp['fields'] == [field.api_name] + assert len(rule_resp['groups_or']) == 1 + assert rule_resp['groups_or'][0]['api_name'] == group_or.api_name + assert len(rule_resp['groups_or'][0]['groups_and']) == 1 + group_and_resp = rule_resp['groups_or'][0]['groups_and'][0] + assert group_and_resp['api_name'] == group_and.api_name + assert group_and_resp['operator'] == FieldSetRuleOperator.SUM_EQUAL + assert group_and_resp['value'] == rule_value assert len(response.data['fields']) == 1 assert response.data['fields'][0]['name'] == field.name @@ -71,11 +79,11 @@ def test_retrieve__fieldset_is_used__return_usage(api_client): # arrange account = create_test_account() user = create_test_owner(account=account) - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '10' shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) template = create_test_template( @@ -109,12 +117,13 @@ def test_retrieve__fieldset_rule_with_fields__ok(api_client): user_1 = create_test_owner(account=account_1) fieldset = create_test_shared_fieldset( account=account_1, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='10', ) field = fieldset.fields.get() - rule = fieldset.rules.get() - rule.fields.add(field) + ruleset = fieldset.rulesets.get() + ruleset.fields.add(field) + group_and = ruleset.groups_or.first().groups_and.first() api_client.token_authenticate(user=user_1) @@ -124,11 +133,14 @@ def test_retrieve__fieldset_rule_with_fields__ok(api_client): # assert assert response.status_code == 200 assert response.data['id'] == fieldset.id - assert len(response.data['rules']) == 1 - assert response.data['rules'][0]['type'] == rule.type - assert response.data['rules'][0]['value'] == rule.value - assert response.data['rules'][0]['api_name'] == rule.api_name - assert response.data['rules'][0]['fields'] == [field.api_name] + assert len(response.data['rulesets']) == 1 + rule_resp = response.data['rulesets'][0] + assert rule_resp['api_name'] == ruleset.api_name + assert rule_resp['fields'] == [field.api_name] + group_or_data = rule_resp['groups_or'][0] + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['operator'] == group_and.operator + assert group_and_data['value'] == group_and.value def test_retrieve__unauthenticated__unauthorized(api_client): @@ -290,7 +302,7 @@ def test_retrieve__not_shared__not_found(api_client): user = create_test_owner(account=account) fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='10', ) fieldset.is_shared = False diff --git a/backend/src/processes/tests/test_views/test_tasks/test_comment.py b/backend/src/processes/tests/test_views/test_tasks/test_comment.py index 0a14eabdf..e2601264a 100644 --- a/backend/src/processes/tests/test_views/test_tasks/test_comment.py +++ b/backend/src/processes/tests/test_views/test_tasks/test_comment.py @@ -7,6 +7,7 @@ CommentStatus, OwnerRole, OwnerType, + PerformerType, ) from src.processes.messages import workflow as messages from src.processes.models.workflows.task import TaskPerformer @@ -21,6 +22,7 @@ from src.processes.tests.fixtures import ( create_test_account, create_test_admin, + create_test_group, create_test_attachment, create_test_guest, create_test_owner, @@ -627,3 +629,57 @@ def test_task_comment__template_starter_own_workflow__ok(api_client, mocker): task=task, text=event.text, ) + + +def test_create__performer_type_group_user__skip(api_client, mocker): + + # arrange + account = create_test_account() + owner = create_test_owner(account=account) + user = create_test_admin(account=account) + group = create_test_group(account=account, users=[user]) + workflow = create_test_workflow(user=owner, tasks_count=1) + task = workflow.tasks.get(number=1) + task.taskperformer_set.all().delete() + TaskPerformer.objects.create( + task_id=task.id, + group_id=group.id, + type=PerformerType.GROUP, + ) + TaskPerformer.objects.create( + task_id=task.id, + user_id=user.id, + type=PerformerType.GROUP_USER, + ) + event = WorkflowEventService.comment_created_event( + text='Test comment', + task=task, + user=owner, + after_create_actions=False, + ) + service_init_mock = mocker.patch.object( + CommentService, + attribute='__init__', + return_value=None, + ) + comment_create_mock = mocker.patch( + 'src.processes.services.events.' + 'CommentService.create', + return_value=event, + ) + api_client.token_authenticate(owner) + + # act + response = api_client.post( + f'/v2/tasks/{task.id}/comment', + data={'text': event.text}, + ) + + # assert + assert response.status_code == 200 + performers = response.data['task']['performers'] + assert len(performers) == 1 + assert performers[0]['type'] == PerformerType.GROUP + assert performers[0]['source_id'] == group.id + service_init_mock.assert_called_once() + comment_create_mock.assert_called_once() diff --git a/backend/src/processes/tests/test_views/test_tasks/test_retrieve.py b/backend/src/processes/tests/test_views/test_tasks/test_retrieve.py index 7130811e8..a564c50b6 100644 --- a/backend/src/processes/tests/test_views/test_tasks/test_retrieve.py +++ b/backend/src/processes/tests/test_views/test_tasks/test_retrieve.py @@ -62,7 +62,6 @@ create_test_owner, create_test_shared_fieldset, create_test_template, - create_test_user, create_test_workflow, ) from src.utils.dates import date_format @@ -190,20 +189,16 @@ def test_retrieve__delayed__ok(api_client, mocker): def test_retrieve__workflow_member__ok(api_client): # arrange - user = create_test_user() - another_user = create_test_user( - account=user.account, - email='admin@test.test', - is_account_owner=False, - ) + account = create_test_account() + user = create_test_owner(account=account) + another_user = create_test_admin(account=account) workflow = create_test_workflow(user) WorkflowPermissionService(workflow).grant_view( user=another_user, source_type=PermissionSource.PERFORMER, source_id=0, ) - tasks = workflow.tasks.order_by('number') - task = tasks[0] + task = workflow.tasks.order_by('number').first() api_client.token_authenticate(another_user) # act @@ -236,7 +231,7 @@ def test_retrieve__account_owner_not_wf_member__ok(api_client): def test_retrieve__admin_not_workflow_member__permission_denied(api_client): # arrange - account = create_test_account(plan=BillingPlanType.PREMIUM) + account = create_test_account() user = create_test_owner(account=account) another_user = create_test_admin(account=account) workflow = create_test_workflow(user=user) @@ -689,6 +684,58 @@ def test_retrieve__performers_type_field__ok(api_client): ] +def test_retrieve__performer_type_group_user__skip(api_client, mocker): + + # arrange + account = create_test_account() + owner = create_test_owner(account=account) + user = create_test_admin(account=account) + group = create_test_group(account=account, users=[user]) + workflow = create_test_workflow(user=user, tasks_count=1) + task = workflow.tasks.get(number=1) + task.taskperformer_set.all().delete() + TaskPerformer.objects.create( + task_id=task.id, + group_id=group.id, + type=PerformerType.GROUP, + ) + TaskPerformer.objects.create( + task_id=task.id, + user_id=user.id, + type=PerformerType.GROUP_USER, + ) + task.due_date = task.date_first_started + timedelta(hours=24) + task.save(update_fields=['due_date']) + + identify_mock = mocker.patch( + 'src.processes.views.task.TaskViewSet.' + 'identify', + ) + group_mock = mocker.patch( + 'src.processes.views.task.TaskViewSet.' + 'group', + ) + api_client.token_authenticate(owner) + + # act + response = api_client.get(f'/v2/tasks/{task.id}') + + # assert + assert response.status_code == 200 + performers = response.data['performers'] + assert len(performers) == 1 + assert performers == [ + { + 'source_id': group.id, + 'type': PerformerType.GROUP, + 'is_completed': False, + 'date_completed_tsp': None, + }, + ] + identify_mock.assert_not_called() + group_mock.assert_not_called() + + def test_retrieve__is_urgent__ok(api_client): # arrange @@ -1650,8 +1697,7 @@ def test_retrieve__user_is_member_in_deleted_task__not_found(api_client): # arrange user = create_test_owner() admin = create_test_admin(account=user.account) - api_client.token_authenticate(admin) - workflow = create_test_workflow(user, tasks_count=1) + workflow = create_test_workflow(user=user, tasks_count=1) WorkflowPermissionService(workflow).grant_view( user=admin, source_type=PermissionSource.PERFORMER, @@ -2031,6 +2077,8 @@ def test_retrieve__task_performer__is_read_only_viewer_false( 'group', ) + api_client.token_authenticate(performer_user) + # act response = api_client.get(f'/v2/tasks/{task.id}') @@ -2055,13 +2103,7 @@ def test_retrieve__workflow_member__ok_read_only( template = create_test_template(user=template_owner) workflow = create_test_workflow(template=template, user=template_owner) task = workflow.tasks.get(number=1) - - member_user = create_test_user( - account=account, - email='member@test.com', - is_account_owner=False, - is_admin=False, - ) + member_user = create_test_not_admin(account=account) WorkflowPermissionService(workflow).grant_view( user=member_user, source_type=PermissionSource.PERFORMER, @@ -2079,6 +2121,8 @@ def test_retrieve__workflow_member__ok_read_only( 'group', ) + api_client.token_authenticate(member_user) + # act response = api_client.get(f'/v2/tasks/{task.id}') diff --git a/backend/src/processes/tests/test_views/test_templates/test_clone/test_fieldsets.py b/backend/src/processes/tests/test_views/test_templates/test_clone/test_fieldsets.py index c3b1351f6..1b22719d9 100644 --- a/backend/src/processes/tests/test_views/test_templates/test_clone/test_fieldsets.py +++ b/backend/src/processes/tests/test_views/test_templates/test_clone/test_fieldsets.py @@ -2,7 +2,7 @@ from src.processes.enums import ( FieldSetLayout, - FieldSetRuleType, + FieldSetRuleOperator, FieldType, LabelPosition, OwnerRole, @@ -100,7 +100,7 @@ def test_clone__kickoff_and_task_fieldsets__ok(api_client): assert clone_fieldsets_1['layout'] == FieldSetLayout.VERTICAL assert clone_fieldsets_1['order'] == 1 assert isinstance(clone_fieldsets_1['fields'], list) - assert clone_fieldsets_1['rules'] == [] + assert clone_fieldsets_1['rulesets'] == [] assert clone_fieldsets_1['shared_fieldset_id'] == shared.id assert len(response.data['tasks'][0]['fieldsets']) == 1 @@ -112,7 +112,7 @@ def test_clone__kickoff_and_task_fieldsets__ok(api_client): assert clone_fieldsets_2['layout'] == FieldSetLayout.VERTICAL assert clone_fieldsets_2['order'] == 2 assert isinstance(clone_fieldsets_2['fields'], list) - assert clone_fieldsets_2['rules'] == [] + assert clone_fieldsets_2['rulesets'] == [] assert clone_fieldsets_2['shared_fieldset_id'] == shared.id @@ -273,12 +273,12 @@ def test_clone__fieldset_with_rules__ok(api_client): shared = create_test_shared_fieldset( account=user.account, name='Fieldset with rules', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) field = shared.fields.first() - rule = shared.rules.first() - field.rules.add(rule) + ruleset = shared.rulesets.first() + ruleset.fields.add(field) api_client.token_authenticate(user) create_response = api_client.post( @@ -324,11 +324,13 @@ def test_clone__fieldset_with_rules__ok(api_client): # assert assert response.status_code == 200 fieldset = response.data['kickoff']['fieldsets'][0] - rules = fieldset['rules'] + rules = fieldset['rulesets'] assert len(rules) == 1 rule_data = rules[0] - assert rule_data['type'] == FieldSetRuleType.SUM_EQUAL - assert rule_data['value'] == '100' + assert rule_data['groups_or'][0]['groups_and'][0]['operator'] == ( + FieldSetRuleOperator.SUM_EQUAL + ) + assert rule_data['groups_or'][0]['groups_and'][0]['value'] == '100' clone_field_api_names = [f['api_name'] for f in fieldset['fields']] assert len(clone_field_api_names) == 1 assert rule_data['fields'] == clone_field_api_names @@ -467,7 +469,7 @@ def test_clone__fieldset_rule_multi_fields__ok(api_client): user = create_test_owner(account=account) shared = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='200', ) shared.fields.all().delete() @@ -485,9 +487,8 @@ def test_clone__fieldset_rule_multi_fields__ok(api_client): type=FieldType.NUMBER, order=2, ) - rule = shared.rules.first() - field_1.rules.add(rule) - field_2.rules.add(rule) + ruleset = shared.rulesets.first() + ruleset.fields.add(field_1, field_2) api_client.token_authenticate(user) create_response = api_client.post( @@ -536,9 +537,11 @@ def test_clone__fieldset_rule_multi_fields__ok(api_client): # assert assert response.status_code == 200 clone_fieldsets = response.data['kickoff']['fieldsets'][0] - rule_data = clone_fieldsets['rules'][0] - assert rule_data['type'] == FieldSetRuleType.SUM_EQUAL - assert rule_data['value'] == '200' + rule_data = clone_fieldsets['rulesets'][0] + assert rule_data['groups_or'][0]['groups_and'][0]['operator'] == ( + FieldSetRuleOperator.SUM_EQUAL + ) + assert rule_data['groups_or'][0]['groups_and'][0]['value'] == '200' clone_fields = clone_fieldsets['fields'] clone_field_api_names = [f['api_name'] for f in clone_fields] assert set(rule_data['fields']) == set(clone_field_api_names) diff --git a/backend/src/processes/tests/test_views/test_templates/test_create/test_fields_rules.py b/backend/src/processes/tests/test_views/test_templates/test_create/test_fields_rules.py new file mode 100644 index 000000000..8a181a5b5 --- /dev/null +++ b/backend/src/processes/tests/test_views/test_templates/test_create/test_fields_rules.py @@ -0,0 +1,148 @@ +import pytest + +from src.processes.enums import ( + FieldRuleOperator, + FieldRuleType, + FieldType, + OwnerRole, + OwnerType, + PerformerType, +) +from src.processes.models.templates.fields import ( + FieldTemplateRuleGroupAnd, + FieldTemplateRuleGroupOr, + FieldTemplateRuleSet, +) +from src.processes.tests.fixtures import ( + create_test_account, create_test_owner, +) + +pytestmark = pytest.mark.django_db + + +def test_create_task_field_rules__active_template_all_data__ok(api_client): + + # arrange + account = create_test_account() + user = create_test_owner(account=account) + field_1_api_name = 'field-1' + field_2_api_name = 'field-2' + ruleset_api_name = 'field-ruleset-1' + group_or_api_name = 'field-group-or-1' + group_and_api_name = 'field-group-and-1' + rule_message = 'Show when value is yes' + rule_order = 1 + rule_value = 'yes' + request_rules = [ + { + 'api_name': ruleset_api_name, + 'type': FieldRuleType.SHOW, + 'message': rule_message, + 'order': rule_order, + 'groups_or': [ + { + 'api_name': group_or_api_name, + 'groups_and': [ + { + 'api_name': group_and_api_name, + 'field': field_1_api_name, + 'operator': FieldRuleOperator.EQUAL, + 'value': rule_value, + }, + ], + }, + ], + }, + ] + api_client.token_authenticate(user) + + # act + response = api_client.post( + '/templates', + data={ + 'name': 'Template', + 'is_active': True, + 'owners': [ + { + 'type': OwnerType.USER, + 'source_id': user.id, + 'role': OwnerRole.OWNER, + }, + ], + 'kickoff': {}, + 'tasks': [ + { + 'number': 1, + 'name': 'First step', + 'raw_performers': [ + { + 'type': PerformerType.USER, + 'source_id': user.id, + }, + ], + 'fields': [ + { + 'type': FieldType.NUMBER, + 'name': 'Number field', + 'order': 1, + 'api_name': field_1_api_name, + }, + { + 'type': FieldType.STRING, + 'name': 'String field', + 'order': 2, + 'api_name': field_2_api_name, + 'rulesets': request_rules, + }, + ], + }, + ], + }, + ) + + # assert + assert response.status_code == 200 + + # FieldTemplate ordering is -order + field_2_data = response.data['tasks'][0]['fields'][0] + assert field_2_data['api_name'] == field_2_api_name + assert len(field_2_data['rulesets']) == 1 + field_1_data = response.data['tasks'][0]['fields'][1] + assert field_1_data['api_name'] == field_1_api_name + assert field_1_data['rulesets'] == [] + + ruleset_data = field_2_data['rulesets'][0] + assert ruleset_data['api_name'] == ruleset_api_name + assert ruleset_data['type'] == FieldRuleType.SHOW + assert ruleset_data['message'] == rule_message + assert ruleset_data['order'] == rule_order + assert len(ruleset_data['groups_or']) == 1 + + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or_api_name + assert len(group_or_data['groups_and']) == 1 + + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and_api_name + assert group_and_data['field'] == field_1_api_name + assert group_and_data['operator'] == FieldRuleOperator.EQUAL + assert group_and_data['value'] == rule_value + + ruleset = FieldTemplateRuleSet.objects.get(api_name=ruleset_api_name) + assert ruleset.type == FieldRuleType.SHOW + assert ruleset.message == rule_message + assert ruleset.order == rule_order + assert ruleset.field.api_name == field_2_api_name + + group_or = FieldTemplateRuleGroupOr.objects.get( + api_name=group_or_api_name, + field_rule=ruleset, + ) + + group_and = FieldTemplateRuleGroupAnd.objects.get( + api_name=group_and_api_name, + group_or=group_or, + ) + assert group_and.field == field_1_api_name + assert group_and.operator == FieldRuleOperator.EQUAL + assert group_and.value == rule_value diff --git a/backend/src/processes/tests/test_views/test_templates/test_create/test_fieldset.py b/backend/src/processes/tests/test_views/test_templates/test_create/test_fieldset.py index 9a847debc..12261649f 100644 --- a/backend/src/processes/tests/test_views/test_templates/test_create/test_fieldset.py +++ b/backend/src/processes/tests/test_views/test_templates/test_create/test_fieldset.py @@ -1,13 +1,10 @@ import pytest from src.processes.enums import ( - FieldSetLayout, - FieldSetRuleType, FieldType, - LabelPosition, OwnerRole, OwnerType, - PerformerType, + PerformerType, FieldSetRuleOperator, ) from src.processes.models.templates.fieldset import FieldsetTemplate from src.processes.models.templates.template import Template @@ -48,30 +45,8 @@ def test_create__kickoff_fieldset_only_required_data__ok( 'src.processes.views.template.' 'AnalyticService.templates_kickoff_created', ) - fs_title = 'Some title' - fs_description = 'Some desc' - fs_name = 'Some name' - fs_order = 3 - label_position = LabelPosition.LEFT - layout = FieldSetLayout.HORIZONTAL - rule_type = FieldSetRuleType.SUM_EQUAL - rule_value = '500' - api_name = 'fs-some-api-name' - shared_fieldset = create_test_shared_fieldset( - title=fs_title, - description=fs_description, - name=fs_name, - order=fs_order, - label_position=label_position, - layout=layout, - rule_type=rule_type, - rule_value=rule_value, - api_name=api_name, - account=account, - ) - shared_field = shared_fieldset.fields.first() - shared_rule = shared_fieldset.rules.first() - shared_rule.fields.add(shared_field) + shared_fieldset = create_test_shared_fieldset(account=account) + shared_fieldset.fields.all().delete() request_data = { 'name': 'Template with fieldset', 'owners': [ @@ -113,44 +88,26 @@ def test_create__kickoff_fieldset_only_required_data__ok( assert response.status_code == 200 template = Template.objects.get(id=response.data['id']) kickoff = template.kickoff_instance - fieldset = FieldsetTemplate.objects.get( + assert FieldsetTemplate.objects.get( kickoff=kickoff, shared_fieldset=shared_fieldset, is_shared=False, ) - field = fieldset.fields.first() - rule = fieldset.rules.first() kickoff_data = response.data['kickoff'] assert len(kickoff_data['fieldsets']) == 1 fieldset_data = kickoff_data['fieldsets'][0] assert fieldset_data['shared_fieldset_id'] == shared_fieldset.id assert fieldset_data['order'] == 0 - assert fieldset_data['title'] == fs_title - assert fieldset_data['description'] == fs_description - assert fieldset_data['name'] == fs_name - assert len(fieldset_data['api_name']) + assert fieldset_data['name'] == shared_fieldset.name + assert fieldset_data['description'] == shared_fieldset.description + assert fieldset_data['title'] == shared_fieldset.name assert fieldset_data['api_name'] != shared_fieldset.api_name - assert fieldset_data['label_position'] == label_position - assert fieldset_data['layout'] == layout - - assert len(fieldset_data['rules']) == 1 - rule_data = fieldset_data['rules'][0] - assert rule_data['type'] == rule_type - assert rule_data['value'] == str(rule_value) - assert rule_data['api_name'] == rule.api_name - assert rule_data['fields'] == [field.api_name] + assert fieldset_data['label_position'] == shared_fieldset.label_position + assert fieldset_data['layout'] == shared_fieldset.layout - assert len(fieldset_data['fields']) == 1 - field_data = fieldset_data['fields'][0] - assert field_data['name'] == shared_field.name - assert field_data['type'] == shared_field.type - assert field_data['order'] == shared_field.order - assert field_data['is_required'] == shared_field.is_required - assert field_data['is_hidden'] == shared_field.is_hidden - assert field_data['description'] == '' - assert field_data['default'] == '' - assert field_data['api_name'] == field.api_name + assert fieldset_data['rulesets'] == [] + assert fieldset_data['fields'] == [] def test_create__kickoff_fieldset_all_fieldset_data__ok( @@ -179,14 +136,16 @@ def test_create__kickoff_fieldset_all_fieldset_data__ok( fs_description = 'Some desc' fs_order = 3 fs_api_name = 'fs-some-api-name' - field_dropdown_api_name = 'field-dropdown-api-name' - field_number_api_name = 'field-number-api-name' - selection_api_name = 'selection-api-name' - rule_api_name = 'rule-api-name' - rule_type = FieldSetRuleType.SUM_EQUAL - rule_value = '100' - shared_fieldset = create_test_shared_fieldset(account=account) - shared_fieldset.fields.all().delete() + shared_fieldset = create_test_shared_fieldset( + account=account, + rule_value='500', + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + ) + shared_field = shared_fieldset.fields.first() + shared_ruleset = shared_fieldset.rulesets.first() + shared_ruleset.fields.add(shared_field) + shared_group_or = shared_ruleset.groups_or.first() + shared_group_and = shared_group_or.groups_and.first() request_data = { 'name': 'Template with fieldset', 'owners': [ @@ -205,34 +164,6 @@ def test_create__kickoff_fieldset_all_fieldset_data__ok( 'title': fs_title, 'description': fs_description, 'api_name': fs_api_name, - 'fields': [ - { - 'name': 'Dropdown field', - 'type': FieldType.DROPDOWN, - 'order': 1, - 'api_name': field_dropdown_api_name, - 'selections': [ - { - 'value': 'Option A', - 'api_name': selection_api_name, - }, - ], - }, - { - 'name': 'Number field', - 'type': FieldType.NUMBER, - 'order': 2, - 'api_name': field_number_api_name, - }, - ], - 'rules': [ - { - 'type': rule_type, - 'value': rule_value, - 'api_name': rule_api_name, - 'fields': [field_number_api_name], - }, - ], }, ], }, @@ -266,13 +197,10 @@ def test_create__kickoff_fieldset_all_fieldset_data__ok( is_shared=False, api_name=fs_api_name, ) - dropdown_field = fieldset.fields.get(api_name=field_dropdown_api_name) - number_field = fieldset.fields.get(api_name=field_number_api_name) - rule = fieldset.rules.get(api_name=rule_api_name) - selection = dropdown_field.selections.get(api_name=selection_api_name) - rule_field = rule.fields.get(api_name=field_number_api_name) - assert selection.value == 'Option A' - assert rule_field == number_field + field = fieldset.fields.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() kickoff_data = response.data['kickoff'] assert len(kickoff_data['fieldsets']) == 1 @@ -286,23 +214,35 @@ def test_create__kickoff_fieldset_all_fieldset_data__ok( assert fieldset_data['label_position'] == shared_fieldset.label_position assert fieldset_data['layout'] == shared_fieldset.layout - # FieldTemplate ordering is -order - assert len(fieldset_data['fields']) == 2 - field_1_data = fieldset_data['fields'][0] - field_2_data = fieldset_data['fields'][1] - selection_data = field_2_data['selections'][0] - assert field_1_data['api_name'] == field_number_api_name - assert field_1_data['type'] == FieldType.NUMBER - assert field_2_data['api_name'] == field_dropdown_api_name - assert field_2_data['type'] == FieldType.DROPDOWN - assert selection_data['api_name'] == selection_api_name - - assert len(fieldset_data['rules']) == 1 - rule_data = fieldset_data['rules'][0] - assert rule_data['type'] == rule_type - assert rule_data['value'] == rule_value - assert rule_data['api_name'] == rule_api_name - assert rule_data['fields'] == [field_number_api_name] + assert len(fieldset_data['rulesets']) == 1 + ruleset_data = fieldset_data['rulesets'][0] + assert ruleset_data['type'] == shared_ruleset.type + assert ruleset_data['api_name'] != shared_ruleset.api_name + assert ruleset_data['fields'] == [field.api_name] + + assert len(ruleset_data['groups_or']) == 1 + groups_or_data = ruleset_data['groups_or'][0] + assert groups_or_data['api_name'] != shared_group_or.api_name + assert groups_or_data['api_name'] == group_or.api_name + + assert len(groups_or_data['groups_and']) == 1 + group_and_data = groups_or_data['groups_and'][0] + assert group_and_data['api_name'] != shared_group_and.api_name + assert group_and_data['api_name'] == group_and.api_name + assert group_and_data['operator'] == group_and.operator + assert group_and_data['value'] == group_and.value + + assert len(fieldset_data['fields']) == 1 + field_data = fieldset_data['fields'][0] + assert field_data['name'] == shared_field.name + assert field_data['type'] == shared_field.type + assert field_data['order'] == shared_field.order + assert field_data['is_required'] == shared_field.is_required + assert field_data['is_hidden'] == shared_field.is_hidden + assert field_data['description'] == '' + assert field_data['default'] == '' + assert field_data['api_name'] != shared_field.api_name + assert field_data['api_name'] == field.api_name def test_create__kickoff_with_empty_fieldsets__no_created( @@ -393,30 +333,9 @@ def test_create__task_fieldset_only_required_data__ok( 'src.processes.views.template.' 'AnalyticService.templates_kickoff_created', ) - fs_title = 'Some title' - fs_description = 'Some desc' - fs_name = 'Some name' - fs_order = 3 - label_position = LabelPosition.LEFT - layout = FieldSetLayout.HORIZONTAL - rule_type = FieldSetRuleType.SUM_EQUAL - rule_value = '500' - api_name = 'fs-some-api-name' - shared_fieldset = create_test_shared_fieldset( - title=fs_title, - description=fs_description, - name=fs_name, - order=fs_order, - label_position=label_position, - layout=layout, - rule_type=rule_type, - rule_value=rule_value, - api_name=api_name, - account=account, - ) - shared_field = shared_fieldset.fields.first() - shared_rule = shared_fieldset.rules.first() - shared_rule.fields.add(shared_field) + shared_fieldset = create_test_shared_fieldset(account=account) + shared_fieldset.fields.all().delete() + request_data = { 'name': 'Template with task fieldset', 'owners': [ @@ -457,44 +376,26 @@ def test_create__task_fieldset_only_required_data__ok( assert response.status_code == 200 template = Template.objects.get(id=response.data['id']) task = template.tasks.first() - fieldset = FieldsetTemplate.objects.get( + assert FieldsetTemplate.objects.get( task=task, shared_fieldset=shared_fieldset, is_shared=False, ) - field = fieldset.fields.first() - rule = fieldset.rules.first() task_data = response.data['tasks'][0] assert len(task_data['fieldsets']) == 1 fieldset_data = task_data['fieldsets'][0] assert fieldset_data['shared_fieldset_id'] == shared_fieldset.id assert fieldset_data['order'] == 0 - assert fieldset_data['title'] == fs_title - assert fieldset_data['description'] == fs_description - assert fieldset_data['name'] == fs_name - assert len(fieldset_data['api_name']) + assert fieldset_data['name'] == shared_fieldset.name + assert fieldset_data['description'] == shared_fieldset.description + assert fieldset_data['title'] == shared_fieldset.name assert fieldset_data['api_name'] != shared_fieldset.api_name - assert fieldset_data['label_position'] == label_position - assert fieldset_data['layout'] == layout - - assert len(fieldset_data['rules']) == 1 - rule_data = fieldset_data['rules'][0] - assert rule_data['type'] == rule_type - assert rule_data['value'] == rule_value - assert rule_data['api_name'] == rule.api_name - assert rule_data['fields'] == [field.api_name] + assert fieldset_data['label_position'] == shared_fieldset.label_position + assert fieldset_data['layout'] == shared_fieldset.layout - assert len(fieldset_data['fields']) == 1 - field_data = fieldset_data['fields'][0] - assert field_data['name'] == shared_field.name - assert field_data['type'] == shared_field.type - assert field_data['order'] == shared_field.order - assert field_data['is_required'] == shared_field.is_required - assert field_data['is_hidden'] == shared_field.is_hidden - assert field_data['description'] == '' - assert field_data['default'] == '' - assert field_data['api_name'] == field.api_name + assert fieldset_data['rulesets'] == [] + assert fieldset_data['fields'] == [] def test_create__task_fieldset_all_fieldset_data__ok( @@ -519,8 +420,17 @@ def test_create__task_fieldset_all_fieldset_data__ok( fs_description = 'Some desc' fs_order = 3 fs_api_name = 'fs-some-api-name' - shared_fieldset = create_test_shared_fieldset(account=account) - shared_fieldset.fields.all().delete() + shared_fieldset = create_test_shared_fieldset( + account=account, + rule_value='500', + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + ) + shared_field = shared_fieldset.fields.first() + shared_ruleset = shared_fieldset.rulesets.first() + shared_ruleset.fields.add(shared_field) + shared_group_or = shared_ruleset.groups_or.first() + shared_group_and = shared_group_or.groups_and.first() + request_data = { 'name': 'Template with task fieldset', 'owners': [ @@ -565,12 +475,16 @@ def test_create__task_fieldset_all_fieldset_data__ok( assert response.status_code == 200 template = Template.objects.get(id=response.data['id']) task = template.tasks.first() - assert FieldsetTemplate.objects.get( + fieldset = FieldsetTemplate.objects.get( task=task, shared_fieldset=shared_fieldset, is_shared=False, api_name=fs_api_name, ) + field = fieldset.fields.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() task_data = response.data['tasks'][0] assert len(task_data['fieldsets']) == 1 @@ -583,8 +497,36 @@ def test_create__task_fieldset_all_fieldset_data__ok( assert fieldset_data['api_name'] == fs_api_name assert fieldset_data['label_position'] == shared_fieldset.label_position assert fieldset_data['layout'] == shared_fieldset.layout - assert fieldset_data['rules'] == [] - assert fieldset_data['fields'] == [] + + assert len(fieldset_data['rulesets']) == 1 + ruleset_data = fieldset_data['rulesets'][0] + assert ruleset_data['type'] == shared_ruleset.type + assert ruleset_data['api_name'] != shared_ruleset.api_name + assert ruleset_data['fields'] == [field.api_name] + + assert len(ruleset_data['groups_or']) == 1 + groups_or_data = ruleset_data['groups_or'][0] + assert groups_or_data['api_name'] != shared_group_or.api_name + assert groups_or_data['api_name'] == group_or.api_name + + assert len(groups_or_data['groups_and']) == 1 + group_and_data = groups_or_data['groups_and'][0] + assert group_and_data['api_name'] != shared_group_and.api_name + assert group_and_data['api_name'] == group_and.api_name + assert group_and_data['operator'] == group_and.operator + assert group_and_data['value'] == group_and.value + + assert len(fieldset_data['fields']) == 1 + field_data = fieldset_data['fields'][0] + assert field_data['name'] == shared_field.name + assert field_data['type'] == shared_field.type + assert field_data['order'] == shared_field.order + assert field_data['is_required'] == shared_field.is_required + assert field_data['is_hidden'] == shared_field.is_hidden + assert field_data['description'] == '' + assert field_data['default'] == '' + assert field_data['api_name'] != shared_field.api_name + assert field_data['api_name'] == field.api_name def test_create__task_with_empty_fieldsets__no_created( @@ -758,7 +700,7 @@ def test_create__draft_fieldset_from_another_account__excluded( title='Private title', description='Private description', name='Private fieldset', - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) request_data = { @@ -819,7 +761,8 @@ def test_create__kickoff_fieldset_with_expanded_fields__preserves_api_names( ): """ Creating an active template with already expanded fieldset fields - must keep provided field/rule api_names (create path, not clone). """ + must keep provided field/ruleset api_names + (create path, not clone). """ # arrange account = create_test_account() @@ -919,7 +862,7 @@ def test_create__task_fieldset_with_expanded_fields__preserves_api_names( ): """ Creating an active template with already expanded task fieldset - fields must keep provided field/rule api_names. """ + fields must keep provided field/ruleset api_names. """ # arrange account = create_test_account() @@ -1072,7 +1015,7 @@ def test_create__kickoff_fieldset_expanded_fields_used_in_task_text__ok( 'default': '', }, ], - 'rules': [], + 'rulesets': [], }, ], }, diff --git a/backend/src/processes/tests/test_views/test_templates/test_public/test_retrieve.py b/backend/src/processes/tests/test_views/test_templates/test_public/test_retrieve.py index ac56ba344..eb6d68487 100644 --- a/backend/src/processes/tests/test_views/test_templates/test_public/test_retrieve.py +++ b/backend/src/processes/tests/test_views/test_templates/test_public/test_retrieve.py @@ -5,7 +5,7 @@ PublicToken, ) from src.processes.enums import ( - FieldType, FieldSetRuleType, + FieldType, FieldSetRuleOperator, ) from src.processes.models.templates.fields import FieldTemplate, \ FieldTemplateSelection @@ -849,7 +849,7 @@ def test_retrieve__kickoff_fieldset__ok( kickoff = template.kickoff_instance shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) fieldset = create_test_fieldset_template( shared_fieldset=shared_fieldset, @@ -860,8 +860,8 @@ def test_retrieve__kickoff_fieldset__ok( order=5, ) fieldset_field = fieldset.fields.first() - rule = fieldset.rules.first() - rule.fields.add(fieldset_field) + ruleset = fieldset.rulesets.first() + ruleset.fields.add(fieldset_field) auth_header_value = ( f'Token {template.embed_id}' ) diff --git a/backend/src/processes/tests/test_views/test_templates/test_run.py b/backend/src/processes/tests/test_views/test_templates/test_run.py index bee6f9c0a..d9389c318 100644 --- a/backend/src/processes/tests/test_views/test_templates/test_run.py +++ b/backend/src/processes/tests/test_views/test_templates/test_run.py @@ -21,7 +21,6 @@ ConditionAction, DirectlyStatus, DueDateRule, - FieldSetRuleType, FieldType, OwnerRole, OwnerType, @@ -31,7 +30,7 @@ WorkflowEventType, WorkflowStatus, LabelPosition, - FieldSetLayout, + FieldSetLayout, FieldSetRuleOperator, ) from src.processes.messages import workflow as messages from src.processes.messages.fieldset import MSG_FS_0002 @@ -5339,7 +5338,7 @@ def test_run__kickoff_with_one_fieldset__ok(mocker, api_client): fs_order = 3 label_position = LabelPosition.LEFT layout = FieldSetLayout.HORIZONTAL - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '100' shared_fieldset = create_test_shared_fieldset( account=account, @@ -5348,7 +5347,7 @@ def test_run__kickoff_with_one_fieldset__ok(mocker, api_client): name=fs_name, label_position=label_position, layout=layout, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) fieldset_template = create_test_fieldset_template( @@ -5587,7 +5586,7 @@ def test_run__kickoff_fieldset_sum_equal__ok( template=template, kickoff=template.kickoff_instance, order=0, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) field_1 = fieldset_template.fields.first() @@ -5602,9 +5601,8 @@ def test_run__kickoff_fieldset_sum_equal__ok( ), account=user.account, ) - rule_template = fieldset_template.rules.first() - field_1.rules.add(rule_template) - field_2.rules.add(rule_template) + ruleset_template = fieldset_template.rulesets.first() + ruleset_template.fields.add(field_1, field_2) wf_run_mock = mocker.patch( 'src.processes.services.workflow_action.' 'WorkflowEventService.workflow_run_event', @@ -5634,7 +5632,7 @@ def test_run__kickoff_fieldset_sum_equal__ok( ) fieldset = kickoff_value.fieldsets.first() assert fieldset.fields.count() == 2 - assert fieldset.rules.count() == 1 + assert fieldset.rulesets.count() == 1 wf_run_mock.assert_called_once() analytics_mock.assert_called_once() @@ -5659,7 +5657,7 @@ def test_run__kickoff_fieldset_sum_equal__validation_error( template=template, kickoff=template.kickoff_instance, order=0, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) field_1 = fieldset_template.fields.first() @@ -5674,9 +5672,8 @@ def test_run__kickoff_fieldset_sum_equal__validation_error( ), account=user.account, ) - rule_template = fieldset_template.rules.first() - field_1.rules.add(rule_template) - field_2.rules.add(rule_template) + ruleset_template = fieldset_template.rulesets.first() + ruleset_template.fields.add(field_1, field_2) wf_run_mock = mocker.patch( 'src.processes.services.workflow_action.' 'WorkflowEventService.workflow_run_event', diff --git a/backend/src/processes/tests/test_views/test_templates/test_titles_by_owners.py b/backend/src/processes/tests/test_views/test_templates/test_titles_by_owners.py index fa2580d79..b5bed9f3d 100644 --- a/backend/src/processes/tests/test_views/test_templates/test_titles_by_owners.py +++ b/backend/src/processes/tests/test_views/test_templates/test_titles_by_owners.py @@ -2,7 +2,7 @@ from src.accounts.enums import BillingPlanType from src.processes.enums import OwnerRole, OwnerType, FieldType, \ - FieldSetRuleType + FieldSetRuleOperator from src.processes.models.templates.fields import ( FieldTemplate, FieldTemplateSelection, @@ -468,12 +468,13 @@ def test_titles_by_owners__kickoff_fieldset__ok(api_client): description='Enter your personal information', api_name='fieldset-personal', order=5, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='1', ) fieldset_field = fieldset.fields.first() - rule = fieldset.rules.all().first() - rule.fields.add(fieldset_field) + ruleset = fieldset.rulesets.first() + ruleset.fields.add(fieldset_field) + group_and = ruleset.groups_or.first().groups_and.first() api_client.token_authenticate(user) # act @@ -491,13 +492,18 @@ def test_titles_by_owners__kickoff_fieldset__ok(api_client): assert fieldset_data['api_name'] == fieldset.api_name assert fieldset_data['label_position'] == fieldset.label_position assert fieldset_data['layout'] == fieldset.layout - assert len(fieldset_data['rules']) == 1 + assert len(fieldset_data['rulesets']) == 1 - rule_data = fieldset_data['rules'][0] - assert rule_data['type'] == rule.type - assert rule_data['value'] == rule.value - assert rule_data['api_name'] == rule.api_name + rule_data = fieldset_data['rulesets'][0] + assert rule_data['type'] == ruleset.type + assert rule_data['api_name'] == ruleset.api_name assert rule_data['fields'] == [fieldset_field.api_name] + assert rule_data['groups_or'][0]['groups_and'][0]['operator'] == ( + group_and.operator + ) + assert rule_data['groups_or'][0]['groups_and'][0]['value'] == ( + group_and.value + ) assert len(fieldset_data['fields']) == 1 field_data = fieldset_data['fields'][0] diff --git a/backend/src/processes/tests/test_views/test_templates/test_update/test_fieldsets.py b/backend/src/processes/tests/test_views/test_templates/test_update/test_fieldsets.py index f1d4b82e6..b58d57d20 100644 --- a/backend/src/processes/tests/test_views/test_templates/test_update/test_fieldsets.py +++ b/backend/src/processes/tests/test_views/test_templates/test_update/test_fieldsets.py @@ -1,13 +1,10 @@ import pytest from src.processes.enums import ( - FieldSetLayout, - FieldSetRuleType, FieldType, - LabelPosition, OwnerRole, OwnerType, - PerformerType, + PerformerType, FieldSetRuleOperator, ) from src.processes.models.templates.fieldset import FieldsetTemplate from src.processes.serializers.templates.template import TemplateSerializer @@ -46,28 +43,11 @@ def test_update__create_kickoff_fieldset_only_required_data__ok( template = create_test_template(user, is_active=True, tasks_count=1) kickoff = template.kickoff_instance task = template.tasks.first() - fs_title = 'Some title' - fs_description = 'Some desc' - fs_name = 'Some name' - fs_order = 3 - label_position = LabelPosition.LEFT - layout = FieldSetLayout.HORIZONTAL - rule_type = FieldSetRuleType.SUM_EQUAL - rule_value = '100' shared_fieldset = create_test_shared_fieldset( account=account, - title=fs_title, - description=fs_description, - name=fs_name, - order=fs_order, - label_position=label_position, - layout=layout, - rule_type=rule_type, - rule_value=rule_value, ) - shared_field = shared_fieldset.fields.first() - shared_rule = shared_fieldset.rules.first() - shared_field.rules.add(shared_rule) + shared_fieldset.fields.all().delete() + shared_fieldset.rulesets.all().delete() mocker.patch( 'src.processes.services.templates.' 'integrations.TemplateIntegrationsService.' @@ -132,36 +112,20 @@ def test_update__create_kickoff_fieldset_only_required_data__ok( shared_fieldset=shared_fieldset, is_shared=False, ) - field = fieldset.fields.first() - rule = fieldset.rules.first() fieldsets = response.data['kickoff']['fieldsets'] assert len(fieldsets) == 1 fieldset_data = fieldsets[0] assert fieldset_data['shared_fieldset_id'] == shared_fieldset.id assert fieldset_data['order'] == 0 - assert fieldset_data['name'] == fs_name - assert fieldset_data['title'] == fs_title - assert fieldset_data['description'] == fs_description - assert fieldset_data['label_position'] == label_position - assert fieldset_data['layout'] == layout + assert fieldset_data['name'] == shared_fieldset.name + assert fieldset_data['title'] == shared_fieldset.name + assert fieldset_data['description'] == shared_fieldset.description + assert fieldset_data['label_position'] == shared_fieldset.label_position + assert fieldset_data['layout'] == shared_fieldset.layout assert fieldset_data['api_name'] == fieldset.api_name - assert len(fieldset_data['fields']) == 1 - field_data = fieldset_data['fields'][0] - assert field_data['name'] == shared_field.name - assert field_data['description'] == '' - assert field_data['type'] == shared_field.type - assert field_data['is_required'] == shared_field.is_required - assert field_data['is_hidden'] == shared_field.is_hidden - assert field_data['order'] == shared_field.order - assert field_data['default'] == shared_field.default - assert field_data['api_name'] == field.api_name - assert len(fieldset_data['rules']) == 1 - rule_data = fieldset_data['rules'][0] - assert rule_data['type'] == rule_type - assert rule_data['value'] == rule_value - assert rule_data['api_name'] == rule.api_name - assert rule_data['fields'] == [field.api_name] + assert fieldset_data['rulesets'] == [] + assert fieldset_data['fields'] == [] def test_update__create_kickoff_fieldset_all_fieldset_data__ok( @@ -179,8 +143,17 @@ def test_update__create_kickoff_fieldset_all_fieldset_data__ok( fs_description = 'Some desc' fs_order = 3 fs_api_name = 'fs-some-api-name' - shared_fieldset = create_test_shared_fieldset(account=account) - shared_fieldset.fields.all().delete() + shared_fieldset = create_test_shared_fieldset( + account=account, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + rule_value='100', + ) + shared_field = shared_fieldset.fields.first() + shared_ruleset = shared_fieldset.rulesets.first() + shared_group_or = shared_ruleset.groups_or.first() + shared_group_and = shared_group_or.groups_and.first() + shared_ruleset.fields.add(shared_field) + mocker.patch( 'src.processes.services.templates.' 'integrations.TemplateIntegrationsService.' @@ -244,26 +217,51 @@ def test_update__create_kickoff_fieldset_all_fieldset_data__ok( # assert assert response.status_code == 200 - assert FieldsetTemplate.objects.get( + fieldset = FieldsetTemplate.objects.get( kickoff=kickoff, shared_fieldset=shared_fieldset, is_shared=False, - api_name=fs_api_name, ) + field = fieldset.fields.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() - fieldsets = response.data['kickoff']['fieldsets'] - assert len(fieldsets) == 1 - fieldset_data = fieldsets[0] + assert len(response.data['kickoff']['fieldsets']) == 1 + fieldset_data = response.data['kickoff']['fieldsets'][0] assert fieldset_data['shared_fieldset_id'] == shared_fieldset.id assert fieldset_data['order'] == fs_order + assert fieldset_data['name'] == shared_fieldset.name assert fieldset_data['title'] == fs_title assert fieldset_data['description'] == fs_description - assert fieldset_data['name'] == shared_fieldset.name - assert fieldset_data['api_name'] == fs_api_name assert fieldset_data['label_position'] == shared_fieldset.label_position assert fieldset_data['layout'] == shared_fieldset.layout - assert fieldset_data['rules'] == [] - assert fieldset_data['fields'] == [] + assert fieldset_data['api_name'] == fieldset.api_name + assert len(fieldset_data['fields']) == 1 + field_data = fieldset_data['fields'][0] + assert field_data['name'] == shared_field.name + assert field_data['description'] == '' + assert field_data['type'] == shared_field.type + assert field_data['is_required'] == shared_field.is_required + assert field_data['is_hidden'] == shared_field.is_hidden + assert field_data['order'] == shared_field.order + assert field_data['default'] == shared_field.default + assert field_data['api_name'] == field.api_name + assert len(fieldset_data['rulesets']) == 1 + + ruleset_data = fieldset_data['rulesets'][0] + assert ruleset_data['type'] == shared_ruleset.type + assert ruleset_data['api_name'] == ruleset.api_name + + assert len(ruleset_data['groups_or']) == 1 + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name + + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + assert group_and_data['operator'] == shared_group_and.operator + assert group_and_data['value'] == shared_group_and.value def test_update__create_kickoff_two_different_fieldsets__ok( @@ -734,7 +732,7 @@ def test_update__update_kickoff__fieldset_all_fields__ok( assert fieldset.label_position == shared_fieldset.label_position -def test_update_kickoff_update_active_template__not_change_fieldset( +def test_update__kickoff_update_active_template__not_change_fs_api_names( mocker, api_client, ): @@ -747,7 +745,7 @@ def test_update_kickoff_update_active_template__not_change_fieldset( task = template.tasks.first() shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) shared_field = shared_fieldset.fields.first() shared_field.type = FieldType.RADIO @@ -767,7 +765,9 @@ def test_update_kickoff_update_active_template__not_change_fieldset( title='Old title', api_name='fs-kickoff-fields', ) - rule = fieldset.rules.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() field = fieldset.fields.first() selection = field.selections.all().first() @@ -827,12 +827,25 @@ def test_update_kickoff_update_active_template__not_change_fieldset( ], }, ], - 'rules': [ + 'rulesets': [ { - 'type': rule.type, - 'value': rule.value, - 'api_name': rule.api_name, + 'type': ruleset.type, + 'api_name': ruleset.api_name, 'fields': [field.api_name], + 'groups_or': [ + { + 'api_name': group_or.api_name, + 'groups_and': [ + { + 'api_name': group_and.api_name, + 'operator': ( + group_and.operator + ), + 'value': group_and.value, + }, + ], + }, + ], }, ], }, @@ -864,21 +877,27 @@ def test_update_kickoff_update_active_template__not_change_fieldset( assert kickoff_fieldset['api_name'] == fieldset.api_name assert kickoff_fieldset['title'] == new_title - rule_data = kickoff_fieldset['rules'][0] - assert rule_data['api_name'] == rule.api_name + ruleset_data = kickoff_fieldset['rulesets'][0] + assert ruleset_data['api_name'] == ruleset.api_name - fields = kickoff_fieldset['fields'] - assert len(fields) == 1 - field_1 = fields[0] - assert field_1['api_name'] == field.api_name + assert len(ruleset_data['groups_or']) == 1 + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name - selections = field_1['selections'] - assert len(selections) == 1 - selection_1 = selections[0] - assert selection_1['api_name'] == selection.api_name + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + assert len(kickoff_fieldset['fields']) == 1 + field_data = kickoff_fieldset['fields'][0] + assert field_data['api_name'] == field.api_name -def test_update_kickoff_update_inactive_template__not_change_fieldset( + assert len(field_data['selections']) == 1 + selection_data = field_data['selections'][0] + assert selection_data['api_name'] == selection.api_name + + +def test_update__kickoff_update_inactive_template__not_change_fs_api_names( mocker, api_client, ): @@ -891,7 +910,7 @@ def test_update_kickoff_update_inactive_template__not_change_fieldset( task = template.tasks.first() shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, ) shared_field = shared_fieldset.fields.first() shared_field.type = FieldType.RADIO @@ -911,7 +930,9 @@ def test_update_kickoff_update_inactive_template__not_change_fieldset( title='Old title', api_name='fs-kickoff-fields', ) - rule = fieldset.rules.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() field = fieldset.fields.first() selection = field.selections.all().first() @@ -982,12 +1003,25 @@ def test_update_kickoff_update_inactive_template__not_change_fieldset( ], }, ], - 'rules': [ + 'rulesets': [ { - 'type': rule.type, - 'value': rule.value, - 'api_name': rule.api_name, + 'type': ruleset.type, + 'api_name': ruleset.api_name, 'fields': [field.api_name], + 'groups_or': [ + { + 'api_name': group_or.api_name, + 'groups_and': [ + { + 'api_name': group_and.api_name, + 'operator': ( + group_and.operator + ), + 'value': group_and.value, + }, + ], + }, + ], }, ], }, @@ -1019,19 +1053,24 @@ def test_update_kickoff_update_inactive_template__not_change_fieldset( assert kickoff_fieldset['api_name'] == fieldset.api_name assert kickoff_fieldset['title'] == new_title - rule_data = kickoff_fieldset['rules'][0] - assert rule_data['api_name'] == rule.api_name + ruleset_data = kickoff_fieldset['rulesets'][0] + assert ruleset_data['api_name'] == ruleset.api_name - fields = kickoff_fieldset['fields'] - assert len(fields) == 1 - field_1 = fields[0] - assert field_1['api_name'] == field.api_name + assert len(ruleset_data['groups_or']) == 1 + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name - selections = field_1['selections'] - assert len(selections) == 1 - selection_1 = selections[0] - assert selection_1['api_name'] == selection.api_name + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + assert len(kickoff_fieldset['fields']) == 1 + field_data = kickoff_fieldset['fields'][0] + assert field_data['api_name'] == field.api_name + + assert len(field_data['selections']) == 1 + selection_data = field_data['selections'][0] + assert selection_data['api_name'] == selection.api_name # Task fieldsets @@ -1047,28 +1086,15 @@ def test_update__create_task_fieldset_only_required_data__ok( template = create_test_template(user, is_active=True, tasks_count=1) kickoff = template.kickoff_instance task = template.tasks.first() - fs_title = 'Some title' - fs_description = 'Some desc' - fs_name = 'Some name' - fs_order = 3 - label_position = LabelPosition.LEFT - layout = FieldSetLayout.HORIZONTAL - rule_type = FieldSetRuleType.SUM_EQUAL + rule_operator = FieldSetRuleOperator.SUM_EQUAL rule_value = '200' shared_fieldset = create_test_shared_fieldset( account=account, - title=fs_title, - description=fs_description, - name=fs_name, - order=fs_order, - label_position=label_position, - layout=layout, - rule_type=rule_type, + rule_operator=rule_operator, rule_value=rule_value, ) - shared_field = shared_fieldset.fields.first() - shared_rule = shared_fieldset.rules.first() - shared_field.rules.add(shared_rule) + shared_fieldset.fields.all().delete() + shared_fieldset.rulesets.all().delete() mocker.patch( 'src.processes.services.templates.' 'integrations.TemplateIntegrationsService.' @@ -1133,36 +1159,20 @@ def test_update__create_task_fieldset_only_required_data__ok( shared_fieldset=shared_fieldset, is_shared=False, ) - field = fieldset.fields.first() - rule = fieldset.rules.first() fieldsets = response.data['tasks'][0]['fieldsets'] assert len(fieldsets) == 1 fieldset_data = fieldsets[0] assert fieldset_data['shared_fieldset_id'] == shared_fieldset.id assert fieldset_data['order'] == 0 - assert fieldset_data['name'] == fs_name - assert fieldset_data['title'] == fs_title - assert fieldset_data['description'] == fs_description - assert fieldset_data['label_position'] == label_position - assert fieldset_data['layout'] == layout + assert fieldset_data['name'] == shared_fieldset.name + assert fieldset_data['title'] == shared_fieldset.name + assert fieldset_data['description'] == shared_fieldset.description + assert fieldset_data['label_position'] == shared_fieldset.label_position + assert fieldset_data['layout'] == shared_fieldset.layout assert fieldset_data['api_name'] == fieldset.api_name - assert len(fieldset_data['fields']) == 1 - field_data = fieldset_data['fields'][0] - assert field_data['name'] == shared_field.name - assert field_data['description'] == '' - assert field_data['type'] == shared_field.type - assert field_data['is_required'] == shared_field.is_required - assert field_data['is_hidden'] == shared_field.is_hidden - assert field_data['order'] == shared_field.order - assert field_data['default'] == shared_field.default - assert field_data['api_name'] == field.api_name - assert len(fieldset_data['rules']) == 1 - rule_data = fieldset_data['rules'][0] - assert rule_data['type'] == rule_type - assert rule_data['value'] == rule_value - assert rule_data['api_name'] == rule.api_name - assert rule_data['fields'] == [field.api_name] + assert fieldset_data['rulesets'] == [] + assert fieldset_data['fields'] == [] def test_update__create_task_fieldset_all_fieldset_data__ok( @@ -1180,8 +1190,17 @@ def test_update__create_task_fieldset_all_fieldset_data__ok( fs_description = 'Some desc' fs_order = 3 fs_api_name = 'fs-some-api-name' - shared_fieldset = create_test_shared_fieldset(account=account) - shared_fieldset.fields.all().delete() + shared_fieldset = create_test_shared_fieldset( + account=account, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + rule_value='100', + ) + shared_field = shared_fieldset.fields.first() + shared_ruleset = shared_fieldset.rulesets.first() + shared_group_or = shared_ruleset.groups_or.first() + shared_group_and = shared_group_or.groups_and.first() + shared_ruleset.fields.add(shared_field) + mocker.patch( 'src.processes.services.templates.' 'integrations.TemplateIntegrationsService.' @@ -1245,26 +1264,53 @@ def test_update__create_task_fieldset_all_fieldset_data__ok( # assert assert response.status_code == 200 - assert FieldsetTemplate.objects.get( + fieldset = FieldsetTemplate.objects.get( task=task, shared_fieldset=shared_fieldset, is_shared=False, api_name=fs_api_name, ) + field = fieldset.fields.first() + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() task_data = response.data['tasks'][0] assert len(task_data['fieldsets']) == 1 fieldset_data = task_data['fieldsets'][0] assert fieldset_data['shared_fieldset_id'] == shared_fieldset.id assert fieldset_data['order'] == fs_order + assert fieldset_data['name'] == shared_fieldset.name assert fieldset_data['title'] == fs_title assert fieldset_data['description'] == fs_description - assert fieldset_data['name'] == shared_fieldset.name - assert fieldset_data['api_name'] == fs_api_name assert fieldset_data['label_position'] == shared_fieldset.label_position assert fieldset_data['layout'] == shared_fieldset.layout - assert fieldset_data['rules'] == [] - assert fieldset_data['fields'] == [] + assert fieldset_data['api_name'] == fieldset.api_name + assert len(fieldset_data['fields']) == 1 + field_data = fieldset_data['fields'][0] + assert field_data['name'] == shared_field.name + assert field_data['description'] == '' + assert field_data['type'] == shared_field.type + assert field_data['is_required'] == shared_field.is_required + assert field_data['is_hidden'] == shared_field.is_hidden + assert field_data['order'] == shared_field.order + assert field_data['default'] == shared_field.default + assert field_data['api_name'] == field.api_name + assert len(fieldset_data['rulesets']) == 1 + + ruleset_data = fieldset_data['rulesets'][0] + assert ruleset_data['type'] == shared_ruleset.type + assert ruleset_data['api_name'] == ruleset.api_name + + assert len(ruleset_data['groups_or']) == 1 + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name + + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + assert group_and_data['operator'] == shared_group_and.operator + assert group_and_data['value'] == shared_group_and.value def test_update__create_task_two_fieldsets__ok( @@ -1807,132 +1853,22 @@ def test_update__update_task__fieldset_all_fields__ok( assert fieldset.label_position == shared_fieldset.label_position -@pytest.mark.parametrize('is_active', (True, False)) -def test_update__update_task_fieldset_all_fields__ok( +def test_update_task_update_template_inactive__not_change_fs_api_names( mocker, - is_active, api_client, ): + # arrange account = create_test_account() user = create_test_owner(account=account) - template = create_test_template(user, is_active=is_active, tasks_count=1) + template = create_test_template(user, tasks_count=1) kickoff = template.kickoff_instance task = template.tasks.get(number=1) - shared_fieldset = create_test_shared_fieldset(account=account) - fieldset = create_test_fieldset_template( + shared_fieldset = create_test_shared_fieldset( account=account, - template=template, - task=task, - shared_fieldset=shared_fieldset, - order=0, - title='Old title', - description='Old desc', - api_name='fs-task-update', - ) - fieldset_id = fieldset.id - new_order = 2 - new_title = 'New title' - new_description = 'New desc' - mocker.patch( - 'src.processes.services.templates.' - 'integrations.TemplateIntegrationsService.' - 'create_integrations_for_template', - ) - mocker.patch( - 'src.processes.services.templates.' - 'integrations.TemplateIntegrationsService.template_updated', - ) - mocker.patch( - 'src.processes.views.template.AnalyticService.templates_updated', - ) - mocker.patch( - 'src.processes.views.template.' - 'AnalyticService.templates_kickoff_updated', - ) - api_client.token_authenticate(user) - - # act - response = api_client.put( - path=f'/templates/{template.id}', - data={ - 'id': template.id, - 'is_active': is_active, - 'name': 'Updated template', - 'owners': [ - { - 'type': OwnerType.USER, - 'source_id': user.id, - 'role': OwnerRole.OWNER, - }, - ], - 'kickoff': { - 'id': kickoff.id, - }, - 'tasks': [ - { - 'id': task.id, - 'api_name': task.api_name, - 'number': task.number, - 'name': task.name, - 'raw_performers': [ - { - 'type': PerformerType.USER, - 'source_id': user.id, - }, - ], - 'fieldsets': [ - { - 'shared_fieldset_id': shared_fieldset.id, - 'api_name': fieldset.api_name, - 'order': new_order, - 'title': new_title, - 'description': new_description, - }, - ], - }, - ], - }, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, + rule_value='10', ) - - # assert - assert response.status_code == 200 - fieldsets = response.data['tasks'][0]['fieldsets'] - assert len(fieldsets) == 1 - task_fieldset_1 = fieldsets[0] - assert task_fieldset_1['api_name'] == fieldset.api_name - assert task_fieldset_1['order'] == new_order - assert task_fieldset_1['title'] == new_title - assert task_fieldset_1['description'] == new_description - assert task_fieldset_1['name'] == fieldset.name - assert task_fieldset_1['layout'] == fieldset.layout - assert task_fieldset_1['label_position'] == fieldset.label_position - if is_active: - fieldset.refresh_from_db() - assert fieldset.id == fieldset_id - assert fieldset.api_name == 'fs-task-update' - assert fieldset.order == new_order - assert fieldset.title == new_title - assert fieldset.description == new_description - assert fieldset.name == shared_fieldset.name - assert fieldset.layout == shared_fieldset.layout - assert fieldset.label_position == shared_fieldset.label_position - - -@pytest.mark.parametrize('is_active', (True, False)) -def test_update_task_update_template__not_change_fieldset( - mocker, - is_active, - api_client, -): - - # arrange - account = create_test_account() - user = create_test_owner(account=account) - template = create_test_template(user, is_active=is_active, tasks_count=1) - kickoff = template.kickoff_instance - task = template.tasks.get(number=1) - shared_fieldset = create_test_shared_fieldset(account=account) shared_field = shared_fieldset.fields.first() shared_field.type = FieldType.RADIO shared_field.save(update_fields=['type']) @@ -1951,6 +1887,9 @@ def test_update_task_update_template__not_change_fieldset( title='Old title', api_name='fieldset-1', ) + ruleset = fieldset.rulesets.first() + group_or = ruleset.groups_or.first() + group_and = group_or.groups_and.first() field = fieldset.fields.first() selection = field.selections.all().first() @@ -2032,14 +1971,29 @@ def test_update_task_update_template__not_change_fieldset( ], }, ], - }, - ], - 'fields': [ - { - 'name': 'New field', - 'api_name': 'field-123', - 'type': FieldType.NUMBER, - 'order': 2, + 'rulesets': [ + { + 'type': ruleset.type, + 'api_name': ruleset.api_name, + 'fields': [field.api_name], + 'groups_or': [ + { + 'api_name': group_or.api_name, + 'groups_and': [ + { + 'api_name': ( + group_and.api_name + ), + 'operator': ( + group_and.operator + ), + 'value': group_and.value, + }, + ], + }, + ], + }, + ], }, ], }, @@ -2052,20 +2006,28 @@ def test_update_task_update_template__not_change_fieldset( fieldsets = response.data['tasks'][0]['fieldsets'] assert len(fieldsets) == 1 - task_fieldset_1 = fieldsets[0] - assert task_fieldset_1['api_name'] == fieldset.api_name - assert task_fieldset_1['title'] == fieldset.title + task_fieldset = fieldsets[0] + assert task_fieldset['api_name'] == fieldset.api_name + assert task_fieldset['title'] == fieldset.title + + ruleset_data = task_fieldset['rulesets'][0] + assert ruleset_data['api_name'] == ruleset.api_name - fields = task_fieldset_1['fields'] - assert len(fields) == 1 - field_1 = fields[0] - assert field_1['api_name'] == field.api_name + assert len(ruleset_data['groups_or']) == 1 + group_or_data = ruleset_data['groups_or'][0] + assert group_or_data['api_name'] == group_or.api_name - selections = field_1['selections'] - assert len(selections) == 1 - selection_1 = selections[0] - assert selection_1['api_name'] == selection.api_name + assert len(group_or_data['groups_and']) == 1 + group_and_data = group_or_data['groups_and'][0] + assert group_and_data['api_name'] == group_and.api_name + + assert len(task_fieldset['fields']) == 1 + field_data = task_fieldset['fields'][0] + assert field_data['api_name'] == field.api_name + assert len(field_data['selections']) == 1 + selection_data = field_data['selections'][0] + assert selection_data['api_name'] == selection.api_name # Mixed task and kickoff @@ -2151,10 +2113,10 @@ def test_update__create_kickoff_and_task_different_fieldsets__ok( task_fieldsets = response.data['tasks'][0]['fieldsets'] assert len(kickoff_fieldsets) == 1 assert len(task_fieldsets) == 1 - kickoff_fieldset_1 = kickoff_fieldsets[0] - task_fieldset_1 = task_fieldsets[0] - assert kickoff_fieldset_1['shared_fieldset_id'] == shared_1.id - assert task_fieldset_1['shared_fieldset_id'] == shared_2.id + kickoff_fieldset = kickoff_fieldsets[0] + task_fieldset = task_fieldsets[0] + assert kickoff_fieldset['shared_fieldset_id'] == shared_1.id + assert task_fieldset['shared_fieldset_id'] == shared_2.id assert kickoff.fieldsets.filter( shared_fieldset_id=shared_1.id, ).count() == 1 @@ -2554,7 +2516,7 @@ def test_update__activate_draft_preserves_kickoff_fieldset_rule_api_names__ok( api_client, ): - """ Draft kickoff fieldset rule/field/selection api_names must be + """ Draft kickoff fieldset ruleset/field/selection api_names must be preserved on activate. """ # arrange @@ -2562,11 +2524,11 @@ def test_update__activate_draft_preserves_kickoff_fieldset_rule_api_names__ok( user = create_test_owner(account=account) shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) shared_number_field = shared_fieldset.fields.first() - shared_rule = shared_fieldset.rules.first() + shared_rule = shared_fieldset.rulesets.first() shared_rule.fields.add(shared_number_field) shared_dropdown_field = FieldTemplate.objects.create( fieldset=shared_fieldset, @@ -2649,8 +2611,8 @@ def test_update__activate_draft_preserves_kickoff_fieldset_rule_api_names__ok( draft_number_api_name = draft_number_data['api_name'] draft_dropdown_api_name = draft_dropdown_data['api_name'] draft_selection_api_name = draft_selection_data['api_name'] - draft_rule_api_name = draft_fieldset['rules'][0]['api_name'] - assert draft_fieldset['rules'][0]['fields'] == [draft_number_api_name] + draft_rule_api_name = draft_fieldset['rulesets'][0]['api_name'] + assert draft_fieldset['rulesets'][0]['fields'] == [draft_number_api_name] # act response = api_client.put( @@ -2701,22 +2663,22 @@ def test_update__activate_draft_preserves_kickoff_fieldset_rule_api_names__ok( selection = dropdown_field.selections.get( api_name=draft_selection_api_name, ) - rule = fieldset.rules.get(api_name=draft_rule_api_name) - rule_field = rule.fields.get(api_name=draft_number_api_name) + ruleset = fieldset.rulesets.get(api_name=draft_rule_api_name) + rule_field = ruleset.fields.get(api_name=draft_number_api_name) assert selection.value == 'Option A' - assert rule.api_name == draft_rule_api_name + assert ruleset.api_name == draft_rule_api_name assert rule_field == number_field fieldset_data = response.data['kickoff']['fieldsets'][0] field_1_data = fieldset_data['fields'][0] field_2_data = fieldset_data['fields'][1] selection_data = field_1_data['selections'][0] - rule_data = fieldset_data['rules'][0] + ruleset_data = fieldset_data['rulesets'][0] assert field_1_data['api_name'] == draft_dropdown_api_name assert field_2_data['api_name'] == draft_number_api_name assert selection_data['api_name'] == draft_selection_api_name - assert rule_data['api_name'] == draft_rule_api_name - assert rule_data['fields'] == [draft_number_api_name] + assert ruleset_data['api_name'] == draft_rule_api_name + assert ruleset_data['fields'] == [draft_number_api_name] def test_update__duplicate_fieldset_api_name__validation_error( @@ -2816,7 +2778,7 @@ def test_update__duplicate_fieldset_rule_api_name__validation_error( api_client, ): - """ Duplicate rule api_name across kickoff and task fieldsets + """ Duplicate ruleset api_name across kickoff and task fieldsets must fail validation. """ # arrange @@ -2827,10 +2789,44 @@ def test_update__duplicate_fieldset_rule_api_name__validation_error( task = template.tasks.first() shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) - fs_rule_api_name = 'fs-rule' + shared_ruleset = shared_fieldset.rulesets.first() + shared_group_and = shared_ruleset.groups_or.first().groups_and.first() + fs_rule_api_name = 'fs-ruleset' + kickoff_ruleset_payload = { + 'api_name': fs_rule_api_name, + 'fields': [], + 'groups_or': [ + { + 'api_name': 'g-or-kickoff', + 'groups_and': [ + { + 'api_name': 'g-and-kickoff', + 'operator': shared_group_and.operator, + 'value': shared_group_and.value, + }, + ], + }, + ], + } + task_ruleset_payload = { + 'api_name': fs_rule_api_name, + 'fields': [], + 'groups_or': [ + { + 'api_name': 'g-or-task', + 'groups_and': [ + { + 'api_name': 'g-and-task', + 'operator': shared_group_and.operator, + 'value': shared_group_and.value, + }, + ], + }, + ], + } mocker.patch( 'src.processes.services.templates.' 'integrations.TemplateIntegrationsService.' @@ -2868,13 +2864,8 @@ def test_update__duplicate_fieldset_rule_api_name__validation_error( 'fieldsets': [ { 'shared_fieldset_id': shared_fieldset.id, - 'rules': [ - { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '100', - 'api_name': fs_rule_api_name, - }, - ], + 'api_name': 'fs-kickoff', + 'rulesets': [kickoff_ruleset_payload], }, ], }, @@ -2893,13 +2884,8 @@ def test_update__duplicate_fieldset_rule_api_name__validation_error( 'fieldsets': [ { 'shared_fieldset_id': shared_fieldset.id, - 'rules': [ - { - 'type': FieldSetRuleType.SUM_EQUAL, - 'value': '100', - 'api_name': fs_rule_api_name, - }, - ], + 'api_name': 'fs-task', + 'rulesets': [task_ruleset_payload], }, ], }, @@ -3243,7 +3229,7 @@ def test_update__add_fieldset_with_expanded_fields__preserves_api_names( ], }, ], - 'rules': [], + 'rulesets': [], }, ], }, @@ -3367,7 +3353,7 @@ def test_update__existing_fieldset_ignores_fields_in_payload__ok( 'default': '', }, ], - 'rules': [], + 'rulesets': [], }, ], }, @@ -3416,11 +3402,13 @@ def test_update__create_fieldset_with_shared_api_names__preserved_in_response( user = create_test_owner(account=account) shared_fieldset = create_test_shared_fieldset( account=account, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', ) shared_number_field = shared_fieldset.fields.first() - shared_rule = shared_fieldset.rules.first() + shared_rule = shared_fieldset.rulesets.first() + shared_group_or = shared_rule.groups_or.first() + shared_group_and = shared_group_or.groups_and.first() shared_dropdown_field = FieldTemplate.objects.create( fieldset=shared_fieldset, account=account, @@ -3507,12 +3495,29 @@ def test_update__create_fieldset_with_shared_api_names__preserved_in_response( ], }, ], - 'rules': [ + 'rulesets': [ { 'type': shared_rule.type, - 'value': shared_rule.value, 'api_name': shared_rule.api_name, 'fields': [shared_number_field.api_name], + 'groups_or': [ + { + 'api_name': shared_group_or.api_name, + 'groups_and': [ + { + 'api_name': ( + shared_group_and.api_name + ), + 'operator': ( + shared_group_and.operator + ), + 'value': ( + shared_group_and.value + ), + }, + ], + }, + ], }, ], }, @@ -3542,11 +3547,11 @@ def test_update__create_fieldset_with_shared_api_names__preserved_in_response( dropdown_data = fieldset_data['fields'][0] number_data = fieldset_data['fields'][1] selection_data = dropdown_data['selections'][0] - rule_data = fieldset_data['rules'][0] + ruleset_data = fieldset_data['rulesets'][0] assert fieldset_data['api_name'] == shared_fieldset.api_name assert number_data['api_name'] != shared_number_field.api_name assert dropdown_data['api_name'] != shared_dropdown_field.api_name assert selection_data['api_name'] != shared_selection.api_name - assert rule_data['api_name'] != shared_rule.api_name - assert rule_data['fields'] != [shared_number_field.api_name] + assert ruleset_data['api_name'] != shared_rule.api_name + assert ruleset_data['fields'] != [shared_number_field.api_name] diff --git a/backend/src/processes/tests/test_views/test_workflow/test_comments/test_create.py b/backend/src/processes/tests/test_views/test_workflow/test_comments/test_create.py index b26839bb2..2d490f937 100644 --- a/backend/src/processes/tests/test_views/test_workflow/test_comments/test_create.py +++ b/backend/src/processes/tests/test_views/test_workflow/test_comments/test_create.py @@ -10,6 +10,7 @@ from src.authentication.services.guest_auth import GuestJWTAuthService from src.processes.enums import ( CommentStatus, + PerformerType, TaskStatus, WorkflowStatus, ) @@ -25,6 +26,8 @@ from src.processes.models.templates.owner import TemplateOwner from src.processes.tests.fixtures import ( create_test_account, + create_test_admin, + create_test_group, create_test_attachment_for_event, create_test_guest, create_test_not_admin, @@ -670,3 +673,60 @@ def test_create_comment__template_starter_other_workflow__forbidden( # assert assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_create_comment__performer_type_group_user__skip( + api_client, + mocker, +): + + # arrange + account = create_test_account() + owner = create_test_owner(account=account) + user = create_test_admin(account=account) + group = create_test_group(account=account, users=[user]) + workflow = create_test_workflow(user=owner, tasks_count=1) + task = workflow.tasks.get(number=1) + task.taskperformer_set.all().delete() + TaskPerformer.objects.create( + task_id=task.id, + group_id=group.id, + type=PerformerType.GROUP, + ) + TaskPerformer.objects.create( + task_id=task.id, + user_id=user.id, + type=PerformerType.GROUP_USER, + ) + event = WorkflowEventService.comment_created_event( + text='Test comment', + task=task, + user=owner, + after_create_actions=False, + ) + service_init_mock = mocker.patch.object( + CommentService, + attribute='__init__', + return_value=None, + ) + comment_create_mock = mocker.patch( + 'src.processes.services.events.' + 'CommentService.create', + return_value=event, + ) + api_client.token_authenticate(owner) + + # act + response = api_client.post( + f'/workflows/{workflow.id}/comment', + data={'text': event.text}, + ) + + # assert + assert response.status_code == 200 + performers = response.data['task']['performers'] + assert len(performers) == 1 + assert performers[0]['type'] == PerformerType.GROUP + assert performers[0]['source_id'] == group.id + service_init_mock.assert_called_once() + comment_create_mock.assert_called_once() diff --git a/backend/src/processes/tests/test_views/test_workflow/test_partial_update.py b/backend/src/processes/tests/test_views/test_workflow/test_partial_update.py index 0d4b26460..3f375e646 100644 --- a/backend/src/processes/tests/test_views/test_workflow/test_partial_update.py +++ b/backend/src/processes/tests/test_views/test_workflow/test_partial_update.py @@ -23,13 +23,12 @@ from src.generics.messages import MSG_GE_0007 from src.processes.enums import ( DueDateRule, - FieldSetRuleType, FieldType, OwnerRole, OwnerType, PerformerType, TaskStatus, - WorkflowEventType, + WorkflowEventType, FieldSetRuleOperator, ) from src.processes.messages.fieldset import MSG_FS_0002 from src.processes.messages.workflow import ( @@ -552,7 +551,7 @@ def test_partial_update__kickoff_fieldset_rule_fail__fields_rolled_back( self, ): """ - Field updates and fieldset rule validation run in one transaction. + Field updates and fieldset ruleset validation run in one transaction. If validate_rules fails, earlier TaskField values are not persisted. """ # arrange @@ -563,7 +562,7 @@ def test_partial_update__kickoff_fieldset_rule_fail__fields_rolled_back( fieldset = create_test_fieldset( workflow=workflow, kickoff=kickoff, - rule_type=FieldSetRuleType.SUM_EQUAL, + rule_operator=FieldSetRuleOperator.SUM_EQUAL, rule_value='100', api_name='kickoff-fieldset', ) @@ -580,8 +579,8 @@ def test_partial_update__kickoff_fieldset_rule_fail__fields_rolled_back( api_name='kickoff-fieldset-field-2', value='40', ) - rule = fieldset.rules.first() - rule.fields.add(field_1, field_2) + ruleset = fieldset.rulesets.first() + ruleset.fields.add(field_1, field_2) serializer = KickoffValueSerializer( instance=kickoff, data={ diff --git a/frontend/src/public/__stubs__/fieldsets.factory.ts b/frontend/src/public/__stubs__/fieldsets.factory.ts index 6225bfd58..5b9f5c857 100644 --- a/frontend/src/public/__stubs__/fieldsets.factory.ts +++ b/frontend/src/public/__stubs__/fieldsets.factory.ts @@ -1,6 +1,6 @@ import { - EFieldLabelPosition, EFieldsetRuleType, IFieldsetBinding, IFieldsetBindingClient, IFieldsetCatalogItem, - IFieldsetField, IFieldsetRuntime, IFieldsetTaskAPI, IFieldsetTemplateRule, + EFieldLabelPosition, EFieldsetNumberRulesetOperator, IFieldsetBinding, IFieldsetBindingClient, IFieldsetCatalogItem, + IFieldsetField, IFieldsetRuntime, IFieldsetTaskAPI, IFieldsetRuleSet, IFieldsetRuleGroupAnd, IFieldsetRuleGroupOr, } from '../types/fieldset'; import { IExtraField } from '../types/template'; @@ -31,11 +31,24 @@ export const makeFieldsetRuntime = (overrides: Partial = {}): }); -export const makeFieldsetTemplateRule = (overrides: Partial = {}): IFieldsetTemplateRule => ({ - apiName: 'rule-1', - type: EFieldsetRuleType.SumEqual, +export const makeFieldsetRuleGroupAnd = (overrides: Partial = {}): IFieldsetRuleGroupAnd => ({ + apiName: 'group-and-1', + operator: EFieldsetNumberRulesetOperator.SumEqual, value: '100', + ...overrides, +}); + +export const makeFieldsetRuleGroupOr = (overrides: Partial = {}): IFieldsetRuleGroupOr => ({ + apiName: 'group-or-1', + groupsAnd: [makeFieldsetRuleGroupAnd()], + ...overrides, +}); + +export const makeFieldsetRuleset = (overrides: Partial = {}): IFieldsetRuleSet => ({ + apiName: 'rule-1', + order: 0, fields: [], + groupsOr: [makeFieldsetRuleGroupOr()], ...overrides, }); @@ -48,7 +61,7 @@ export const makeFieldsetCatalogItem = (overrides: Partial labelPosition: EFieldLabelPosition.Top, layout: 'vertical', order: 0, - rules: [], + rulesets: [], fields: [], usage: [], ...overrides, @@ -63,7 +76,7 @@ export const makeFieldsetBindingClient = (overrides: Partial = {}): labelPosition: EFieldLabelPosition.Top, layout: 'vertical', order: 0, - rules: [], + rulesets: [], fields: [], ...overrides, }); diff --git a/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx b/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx index 7e18bc2af..10a175d3d 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx @@ -23,7 +23,7 @@ export function FieldsetCard({ layout, order, title, - rules, + rulesets = [], fields, usage, }: IFieldsetCatalogItem) { @@ -42,7 +42,7 @@ export function FieldsetCard({ layout, order, title, - rules, + rulesets, fields, usage, })); @@ -61,12 +61,11 @@ export function FieldsetCard({ }; const isLinked = Boolean(usage && usage.length > 0); - const hasContent = fields.length > 0 || rules.length > 0; + const hasContent = fields.length > 0 || rulesets.length > 0; return (
-
)} - {rules.length > 0 && ( + {rulesets.length > 0 && (
{formatMessage( { id: 'fieldsets.stats.rules' }, - { count: rules.length }, + { count: rulesets.length }, )}
)} diff --git a/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx b/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx index f48e15c29..40fdcfa31 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx @@ -15,7 +15,7 @@ import { import { history } from '../../../../utils/history'; import { intlMock } from '../../../../__stubs__/intlMock'; import { IFieldsetField } from '../../../../types/fieldset'; -import { makeFieldsetCatalogItem, makeFieldsetTemplateRule } from '../../../../__stubs__/fieldsets.factory'; +import { makeFieldsetCatalogItem, makeFieldsetRuleset } from '../../../../__stubs__/fieldsets.factory'; jest.mock('../../../../utils/history', () => ({ history: { push: jest.fn(), location: { pathname: '/' }, listen: jest.fn() }, @@ -119,7 +119,7 @@ describe('FieldsetCard', () => { layout: props.layout, order: props.order, title: props.title, - rules: props.rules, + rulesets: props.rulesets, fields: props.fields, usage: props.usage, }), @@ -176,7 +176,7 @@ describe('FieldsetCard', () => { it('shows field count and rule count when both are > 0', () => { const props = makeProps({ fields: [makeField(), makeField()], - rules: [makeFieldsetTemplateRule()], + rulesets: [makeFieldsetRuleset()], }); render(React.createElement(FieldsetCard, props)); @@ -185,21 +185,21 @@ describe('FieldsetCard', () => { }); it('hides footer when fields and rules are both empty', () => { - render(React.createElement(FieldsetCard, makeProps({ fields: [], rules: [] }))); + render(React.createElement(FieldsetCard, makeProps({ fields: [], rulesets: [] }))); expect(screen.queryByText(FIELDS_STATS(0))).not.toBeInTheDocument(); expect(screen.queryByText(RULES_STATS(0))).not.toBeInTheDocument(); }); it('shows only fields count when rules are empty', () => { - render(React.createElement(FieldsetCard, makeProps({ fields: [makeField()], rules: [] }))); + render(React.createElement(FieldsetCard, makeProps({ fields: [makeField()], rulesets: [] }))); expect(screen.getByText(FIELDS_STATS(1))).toBeInTheDocument(); expect(screen.queryByText(RULES_STATS(0))).not.toBeInTheDocument(); }); it('shows only rules count when fields are empty', () => { - render(React.createElement(FieldsetCard, makeProps({ fields: [], rules: [makeFieldsetTemplateRule()] }))); + render(React.createElement(FieldsetCard, makeProps({ fields: [], rulesets: [makeFieldsetRuleset()] }))); expect(screen.getByText(RULES_STATS(1))).toBeInTheDocument(); expect(screen.queryByText(FIELDS_STATS(0))).not.toBeInTheDocument(); diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css index d1f0411f4..d0b30f11d 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css @@ -71,16 +71,14 @@ } /* stylelint-disable declaration-no-important */ -.settings-description { - padding: 1.2rem 1.6rem; +.settings-description, +.settings-title { display: block; box-sizing: border-box; width: 100% !important; max-width: 100%; - min-height: 8rem; font-family: inherit; - resize: vertical; - border: 1px solid var(--pneumatic-color-black16); + border: 0.1rem solid var(--pneumatic-color-black16); border-radius: 0.8rem; outline: none; transition: border-color 0.2s; @@ -100,18 +98,74 @@ color: var(--pneumatic-color-black72); background-color: var(--pneumatic-color-black4); border: 0.1rem solid transparent; + opacity: 1; field-sizing: content; + } + + &::-webkit-resizer { + display: none !important; + } +} + +.settings-description { + padding: 1.2rem 1.6rem; + min-height: 8rem; + resize: vertical; +} + +.settings-title { + padding: 0.8rem 1.2rem; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + resize: none !important; + + &:disabled { + overflow: visible; + } + + &:has(:disabled), + &:has(button:disabled), + &[class*='filter-select_disabled'] { + cursor: default; + background-color: var(--pneumatic-color-black4); + border: 0.1rem solid transparent; opacity: 1; + + button, + span { + color: var(--pneumatic-color-black72); + } + + svg { + color: var(--pneumatic-color-black32); + } } } /* stylelint-enable declaration-no-important */ -.settings-select { +.readonly-badge { + margin-left: 1.2rem; + padding: 0.2rem 0.8rem; + font-size: 1.2rem; + font-weight: 600; + vertical-align: middle; + color: var(--pneumatic-color-black72); + background-color: var(--pneumatic-color-black4); + border-radius: 0.4rem; + opacity: 0.55; +} +.settings-title_error { + border-color: var(--pneumatic-color-warning); +} + +.settings-select, +.rule-operator-select { padding: 0.8rem 1.2rem; width: fit-content; min-width: 15rem; background-color: var(--pneumatic-color-white); - border: 1px solid var(--pneumatic-color-black16); + border: 0.1rem solid var(--pneumatic-color-black16); border-radius: 0.8rem; transition: border-color 0.2s; @@ -140,7 +194,8 @@ } /* stylelint-disable declaration-no-important */ -.settings-select__toggle { +.settings-select__toggle, +.rule-operator-select__toggle { @mixin text-base; font-weight: normal !important; @@ -152,14 +207,18 @@ } .settings-select__toggle:focus, -.settings-select__toggle:focus-visible { +.settings-select__toggle:focus-visible, +.rule-operator-select__toggle:focus, +.rule-operator-select__toggle:focus-visible { outline: none !important; box-shadow: none !important; } -.settings-select__menu { +.settings-select__menu, +.rule-operator-select__menu { margin-left: 0 !important; width: 100%; + min-width: 16rem; button:focus, button:focus-visible, @@ -182,57 +241,6 @@ opacity: 0.55; } -/* stylelint-disable declaration-no-important */ -.settings-title { - padding: 0.8rem 1.2rem; - display: block; - overflow: hidden; - box-sizing: border-box; - width: 100% !important; - max-width: 100%; - font-family: inherit; - white-space: nowrap; - text-overflow: ellipsis; - resize: none !important; - border: 1px solid var(--pneumatic-color-black16); - border-radius: 0.8rem; - outline: none; - transition: border-color 0.2s; - - @mixin text-base; - - &:focus { - border-color: var(--pneumatic-color-blue); - } - - &:disabled { - overflow: visible; - min-height: unset; - white-space: pre-wrap; - word-break: break-word; - cursor: default; - resize: none !important; - color: var(--pneumatic-color-black72); - background-color: var(--pneumatic-color-black4); - border: 0.1rem solid transparent; - field-sizing: content; - opacity: 1; - } - - &::-webkit-resizer { - display: none !important; - } -} - -.settings-description::-webkit-resizer { - display: none !important; -} -/* stylelint-enable declaration-no-important */ - -.settings-title_error { - border-color: var(--pneumatic-color-warning); -} - .empty-text { color: var(--pneumatic-color-black48); @@ -270,7 +278,7 @@ resize: none !important; color: var(--pneumatic-color-black72) !important; background-color: var(--pneumatic-color-black4) !important; - border: 1px solid transparent !important; + border: 0.1rem solid transparent !important; border-radius: 0.8rem !important; opacity: 1 !important; } @@ -395,14 +403,14 @@ font-weight: 600; text-align: left; color: var(--pneumatic-color-black48); - border-bottom: 2px solid var(--pneumatic-color-black16); + border-bottom: 0.2rem solid var(--pneumatic-color-black16); @mixin text-small; } .fields-table td { padding: 1.2rem 1.6rem; - border-bottom: 1px solid var(--pneumatic-color-black16); + border-bottom: 0.1rem solid var(--pneumatic-color-black16); @mixin text-base; } @@ -412,16 +420,16 @@ } .rule-row { - padding: 1.2rem 0; + padding: 0 2.8rem 0.4rem 0; display: flex; align-items: center; flex-wrap: wrap; gap: 1.2rem; - border-bottom: 1px solid var(--pneumatic-color-black16); -} + border-bottom: 0.1rem solid var(--pneumatic-color-black16); -.rule-row:last-child { - border-bottom: none; + &:hover .rule-remove-btn { + opacity: 1; + } } .rule-fields-selector { @@ -431,12 +439,16 @@ flex-basis: 100%; } +.rule-fields-tooltip-wrapper { + display: inline-flex; +} + .rule-fields-label { font-weight: 600; white-space: nowrap; color: var(--pneumatic-color-black72); - @mixin text-small; + @mixin text-base; } .rule-fields-select { @@ -466,10 +478,10 @@ @mixin text-base; } -.rule-value-input { - flex: 1; +.rule-value-input, +.ruleset-message-input { padding: 0.8rem 1.2rem; - border: 1px solid var(--pneumatic-color-black16); + border: 0.1rem solid var(--pneumatic-color-black16); border-radius: 0.8rem; outline: none; transition: border-color 0.2s; @@ -479,7 +491,6 @@ &:focus { border-color: var(--pneumatic-color-blue); } - &:disabled { cursor: default; color: var(--pneumatic-color-black72); @@ -489,39 +500,192 @@ } } -.rule-delete-btn { - padding: 0.6rem 1.2rem; +.rule-value-input { + flex: 1; +} + +.ruleset-message-input { + width: 100%; +} + +.rule-row { + padding: 0 2.8rem 0.4rem 0; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1.2rem; + border-bottom: 0.1rem solid var(--pneumatic-color-black16); + + &:last-child { + border-bottom: none; + } + + &:hover .rule-remove-btn { + opacity: 1; + } +} + +.rule-item { + position: relative; +} + +.rule-remove-btn { + position: absolute; + top: 1rem; + right: 0; + padding: 0; cursor: pointer; - color: var(--pneumatic-color-black48); + color: var(--pneumatic-color-notification1); + background: transparent; + border: 0; + opacity: 0; + transition: opacity 0.15s; + + @media (hover: none) { + opacity: 1; + } +} + +.rule-item__combinator { + margin: 0.4rem 0 0.8rem; + width: 10rem; +} + +.select-rule-combinator { + display: inline-block; + background: var(--pneumatic-color-black4); +} + +.select-toggle { + width: auto; + font-weight: normal; + text-transform: uppercase; +} + +.add-rule-btn { + padding: 0; + display: block; + font-size: 1.3rem; + line-height: 1.6rem; + cursor: pointer; + color: var(--pneumatic-color-link-dark); background: none; - border: 1px solid var(--pneumatic-color-black16); - border-radius: 0.8rem; - transition: all 0.2s; + border: none; + transition: 0.1s ease-in-out all; - @mixin text-small; + &:hover { + color: var(--pneumatic-color-link-hover); + } - &:hover:not(:disabled) { - color: var(--pneumatic-color-red); - border-color: var(--pneumatic-color-red); + &:active { + transform: scale(0.98); } +} - &:disabled { - cursor: default; - color: var(--pneumatic-color-black72); +.ruleset-card { + position: relative; + margin-bottom: 1.6rem; + padding: 1.6rem; + background-color: var(--pneumatic-color-white); + border: 0.1rem solid var(--pneumatic-color-black16); + border-radius: 1.2rem; +} + +.ruleset-card__footer { + margin-top: 1.2rem; + display: flex; + align-items: center; + justify-content: flex-end; +} + +.ruleset-card__delete-btn { + padding: 0.6rem 1.2rem; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 500; + cursor: pointer; + color: var(--pneumatic-color-black72); + background: transparent; + border: 0.1rem solid var(--pneumatic-color-black16); + border-radius: 0.8rem; + transition: all 0.2s ease; + + @mixin text-base; + + &:hover { + color: var(--pneumatic-color-black); background-color: var(--pneumatic-color-black4); - border: 0.1rem solid transparent; - opacity: 0.55; + border-color: var(--pneumatic-color-black32); + } + + &:active { + transform: scale(0.98); } } -.add-rule-btn { +.ruleset-card__field { + margin-bottom: 1.2rem; + display: flex; + align-items: center; + gap: 0.8rem; +} + +.ruleset-card__label { + margin: 1.2rem 0 0.4rem; + display: block; + font-size: 1.2rem; + font-weight: 500; + line-height: 1.6rem; + color: var(--pneumatic-color-black48); + + &:first-of-type { + margin-top: 0; + } +} + +.field-tags-list { + margin-top: 0.8rem; + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.field-tag { + padding: 0.4rem 0.8rem; + display: inline-flex; + align-items: center; + background-color: var(--pneumatic-color-black4); + border-radius: 1.6rem; + + @mixin text-small; +} + +.field-tag__name { + margin: 0; +} + +.field-tag__close { + margin-left: 0.4rem; + display: flex; + align-items: center; + cursor: pointer; + color: var(--pneumatic-color-black16); + transition: color 0.1s ease; + + &:hover { + color: var(--pneumatic-color-link-hover); + } +} + +.add-ruleset-btn { margin-top: 1.6rem; padding: 0.8rem 1.6rem; font-weight: 600; cursor: pointer; color: var(--pneumatic-color-blue); background: none; - border: 1px dashed var(--pneumatic-color-black16); + border: 0.1rem dashed var(--pneumatic-color-black16); border-radius: 0.8rem; transition: all 0.2s; @@ -550,7 +714,74 @@ var(--pneumatic-color-skeleton-gray-bg) ); background-repeat: no-repeat; - background-size: 200px 100%; + background-size: 20rem 100%; +} + +.usage-banner { + margin-bottom: 2rem; + padding: 1.2rem 2rem; + display: flex; + align-items: center; + min-height: 5.6rem; + border-radius: 0.8rem; + + @mixin text-base; +} + +.usage-banner--linked { + background-color: var(--pneumatic-color-beige); +} + +.usage-banner--not-linked { + color: var(--pneumatic-color-black80); + background-color: var(--pneumatic-success-color-light); +} + +.usage-banner__row { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +/* stylelint-disable declaration-no-important, selector-max-compound-selectors, selector-max-specificity */ +.usage-banner .usage-banner__row .usage-banner__dropdown .usage-banner__menu-list { + min-width: 24rem !important; + max-width: 24rem; + + :global(.ps) { + max-height: 20.4rem !important; + } +} + +.usage-banner .usage-banner__row .usage-banner__dropdown .usage-banner__option { + display: block !important; + + @mixin text-truncate; +} +/* stylelint-enable declaration-no-important, selector-max-compound-selectors, selector-max-specificity */ + +.usage-banner__option-tooltip { + span { + display: block; + width: 100%; + + @mixin text-truncate; + } +} + +.usage-banner__tooltip-content { + padding: 0.4rem 0.8rem; + text-align: center; +} + +.usage-banner__dropdown button { + background: none; + + p { + font-weight: normal; + color: var(--pneumatic-color-link-dark); + } } .usage-banner { diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx index 1e54216ed..cb82774cd 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx @@ -20,7 +20,7 @@ import { import { history } from '../../../utils/history'; import { ERoutes } from '../../../constants/routes'; -import { ModifyDropdown, Button, FilterSelect, Tooltip } from '../../UI'; +import { ModifyDropdown, Button, Tooltip, FilterSelect } from '../../UI'; import { EModifyDropdownToggle } from '../../UI/ModifyDropdown/types'; import { DropdownList } from '../../UI/DropdownList'; import { NotificationManager } from '../../UI/Notifications'; @@ -36,9 +36,8 @@ import { EExtraFieldMode, EExtraFieldType, IExtraField } from '../../../types/te import { FilledInfoIcon, ArrowDropdownIcon, DateIcon, LinkIcon } from '../../icons'; import { EInputNameBackgroundColor, EMoveDirections } from '../../../types/workflow'; import { - IFieldsetTemplateRule, + IFieldsetRuleSet, EFieldLabelPosition, - EFieldsetRuleType, IUpdateFieldsetParams, } from '../../../types/fieldset'; import { ExtraFieldsMap } from '../../TemplateEdit/ExtraFields/utils/ExtraFieldsMap'; @@ -52,15 +51,11 @@ import { useDatasetOptions } from '../../TemplateEdit/ExtraFields/utils/useDatas import { normalizeFieldsForUI } from './fieldsetFieldMappers'; import { SINGLE_LINE_FIELD_TYPES } from './constants'; import { validateFieldsetRules } from '../validators'; -import { - FIELDSET_LABEL_POSITION_OPTIONS, - FIELDSET_RULE_TYPES, - FIELDSET_RULE_VALUE_PLACEHOLDER_BY_TYPE, -} from '../constants'; +import { FIELDSET_LABEL_POSITION_OPTIONS } from '../constants'; import { useCheckDevice } from '../../../hooks/useCheckDevice'; - -import { TFieldsetDetailsProps, TDetailFieldsetState, TDetailFieldsetChanges } from './types'; +import { TFieldsetDetailsProps, TLocalFieldsetState, TFieldsetChanges } from './types'; +import { FieldsetRulesets } from './FieldsetRulesets/FieldsetRulesets'; import styles from './FieldsetDetails.css'; const READONLY_FIELD_ICONS: Partial>>> = { @@ -69,12 +64,12 @@ const READONLY_FIELD_ICONS: Partial(EMPTY_DETAIL_FIELDSET); - const [detailFieldsetChanges, setDetailFieldsetChanges] = useState({}); - const datasetOptions = useDatasetOptions(detailFieldset.fields); + const [localFieldset, setLocalFieldset] = useState(EMPTY_LOCAL_FIELDSET); + const [fieldsetChanges, setFieldsetChanges] = useState({}); + const datasetOptions = useDatasetOptions(localFieldset.fields); const labelPositionRef = useRef(null); const fieldsetListRoute = ERoutes.Fieldsets; - const isChanged = Object.keys(detailFieldsetChanges).length > 0; + const isChanged = Object.keys(fieldsetChanges).length > 0; useEffect(() => { const id = Number(matchParamId); @@ -118,15 +113,15 @@ const FieldsetDetails = ({ useEffect(() => { if (!fieldset) return; - setDetailFieldset({ + setLocalFieldset({ title: fieldset.title, description: fieldset.description || '', labelPosition: fieldset.labelPosition, fields: normalizeFieldsForUI(fieldset.fields as unknown as IExtraField[]), - rules: fieldset.rules || [], + rulesets: fieldset.rulesets || [], }); - setDetailFieldsetChanges({}); - }, [fieldset?.id, fieldset?.title, fieldset?.description, fieldset?.labelPosition, fieldset?.fields, fieldset?.rules]); + setFieldsetChanges({}); + }, [fieldset?.id, fieldset?.title, fieldset?.description, fieldset?.labelPosition, fieldset?.fields, fieldset?.rulesets]); const labelPositionOptions = useMemo( () => @@ -137,102 +132,58 @@ const FieldsetDetails = ({ [formatMessage], ); - const ruleTypeOptions = useMemo( - () => - FIELDSET_RULE_TYPES.map((option) => ({ - id: option.value, - name: formatMessage({ id: option.labelKey }), - })), - [formatMessage], - ); - const handleSettingsTitleChange = (event: ChangeEvent) => { const title = event.target.value; - setDetailFieldset((prev) => ({ ...prev, title })); - setDetailFieldsetChanges((prev) => ({ ...prev, title })); + setLocalFieldset((prev) => ({ ...prev, title })); + setFieldsetChanges((prev) => ({ ...prev, title })); }; const handleSettingsDescriptionChange = (event: ChangeEvent) => { const description = event.target.value; - setDetailFieldset((prev) => ({ ...prev, description })); - setDetailFieldsetChanges((prev) => ({ ...prev, description })); + setLocalFieldset((prev) => ({ ...prev, description })); + setFieldsetChanges((prev) => ({ ...prev, description })); }; const getSortedFields = useCallback(() => { - return [...detailFieldset.fields].sort((a, b) => b.order - a.order); - }, [detailFieldset.fields]); + return [...localFieldset.fields].sort((a, b) => b.order - a.order); + }, [localFieldset.fields]); const sortedFields = useMemo(() => getSortedFields(), [getSortedFields]); const handleCreateField = (type: EExtraFieldType) => { - const newFields = getNormalizeFieldsOrders([...detailFieldset.fields, getEmptyField(type, formatMessage)]); - setDetailFieldset((prev) => ({ ...prev, fields: newFields })); - setDetailFieldsetChanges((prev) => ({ ...prev, fields: newFields })); + const newFields = getNormalizeFieldsOrders([...localFieldset.fields, getEmptyField(type, formatMessage)]); + setLocalFieldset((prev) => ({ ...prev, fields: newFields })); + setFieldsetChanges((prev) => ({ ...prev, fields: newFields })); }; const handleEditField = (apiName: string) => (changedProps: Partial) => { const newFields = getEditedFields(getSortedFields(), apiName, changedProps); - setDetailFieldset((prev) => ({ ...prev, fields: newFields })); - setDetailFieldsetChanges((prev) => ({ ...prev, fields: newFields })); + setLocalFieldset((prev) => ({ ...prev, fields: newFields })); + setFieldsetChanges((prev) => ({ ...prev, fields: newFields })); }; const handleDeleteField = (idx: number) => { const newFields = getNormalizeFieldsOrders(getSortedFields().filter((_, index) => index !== idx)); - setDetailFieldset((prev) => ({ ...prev, fields: newFields })); - setDetailFieldsetChanges((prev) => ({ ...prev, fields: newFields })); + setLocalFieldset((prev) => ({ ...prev, fields: newFields })); + setFieldsetChanges((prev) => ({ ...prev, fields: newFields })); }; const handleMoveField = (from: number, direction: EMoveDirections) => { const to = direction === EMoveDirections.Up ? from - 1 : from + 1; const newFields = moveWorkflowField(from, to, getSortedFields()); - setDetailFieldset((prev) => ({ ...prev, fields: newFields })); - setDetailFieldsetChanges((prev) => ({ ...prev, fields: newFields })); - }; - - const handleAddRule = () => { - const newRule: IFieldsetTemplateRule = { - apiName: `temporary-${Date.now()}`, - type: FIELDSET_RULE_TYPES[0].value, - value: '', - fields: [], - }; - const rules = [...detailFieldset.rules, newRule]; - setDetailFieldset((prev) => ({ ...prev, rules })); - setDetailFieldsetChanges((prev) => ({ ...prev, rules })); - }; - - const handleEditRuleValue = (index: number, value: string) => { - const rules = detailFieldset.rules.map((rule, i) => (i === index ? { ...rule, value } : rule)); - setDetailFieldset((prev) => ({ ...prev, rules })); - setDetailFieldsetChanges((prev) => ({ ...prev, rules })); - }; - - const handleEditRuleType = (index: number, type: EFieldsetRuleType) => { - const rules = detailFieldset.rules.map((rule, i) => (i === index ? { ...rule, type } : rule)); - setDetailFieldset((prev) => ({ ...prev, rules })); - setDetailFieldsetChanges((prev) => ({ ...prev, rules })); + setLocalFieldset((prev) => ({ ...prev, fields: newFields })); + setFieldsetChanges((prev) => ({ ...prev, fields: newFields })); }; - const handleEditRuleFields = (index: number, fieldApiNames: (string | number | null)[]) => { - const rules = detailFieldset.rules.map((rule, i) => - i === index - ? { ...rule, fields: fieldApiNames.filter((name): name is string => typeof name === 'string') } - : rule, - ); - setDetailFieldset((prev) => ({ ...prev, rules })); - setDetailFieldsetChanges((prev) => ({ ...prev, rules })); - }; - - const handleDeleteRule = (index: number) => { - const rules = detailFieldset.rules.filter((_, i) => i !== index); - setDetailFieldset((prev) => ({ ...prev, rules })); - setDetailFieldsetChanges((prev) => ({ ...prev, rules })); + const handleRulesetsChange = (rulesets: IFieldsetRuleSet[]) => { + setLocalFieldset((prev) => ({ ...prev, rulesets })); + setFieldsetChanges((prev) => ({ ...prev, rulesets })); }; const handleSave = (onSuccess?: () => void): void => { if (!fieldset || !isChanged) return; - const titleErrorMessageKey = validateFieldsetTitle(detailFieldset.title); + const titleErrorMessageKey = validateFieldsetTitle(localFieldset.title); if (titleErrorMessageKey) { NotificationManager.warning({ @@ -241,8 +192,8 @@ const FieldsetDetails = ({ return; } - if (detailFieldsetChanges.rules) { - const ruleErrorMessageKey = validateFieldsetRules(detailFieldsetChanges.rules, detailFieldset.fields); + if (fieldsetChanges.rulesets) { + const ruleErrorMessageKey = validateFieldsetRules(fieldsetChanges.rulesets, localFieldset.fields); if (ruleErrorMessageKey) { NotificationManager.warning({ @@ -257,32 +208,31 @@ const FieldsetDetails = ({ onSuccess, }; - if (detailFieldsetChanges.title !== undefined) { - payload.title = detailFieldsetChanges.title; + if (fieldsetChanges.title !== undefined) { + payload.title = fieldsetChanges.title; } - if (detailFieldsetChanges.description !== undefined) { - payload.description = detailFieldsetChanges.description; + if (fieldsetChanges.description !== undefined) { + payload.description = fieldsetChanges.description; } - if (detailFieldsetChanges.labelPosition) { - payload.labelPosition = detailFieldsetChanges.labelPosition; + if (fieldsetChanges.labelPosition) { + payload.labelPosition = fieldsetChanges.labelPosition; } - if (detailFieldsetChanges.fields) { - payload.fields = detailFieldsetChanges.fields.map( + if (fieldsetChanges.fields) { + payload.fields = fieldsetChanges.fields.map( ({ id: _id, ...rest }) => rest, ) as IUpdateFieldsetParams['fields']; } - if (detailFieldsetChanges.rules) { - payload.rules = detailFieldsetChanges.rules.map(({ apiName, ...rule }) => ({ - ...rule, - ...(apiName.startsWith('temporary-') ? {} : { apiName }), - })) as IFieldsetTemplateRule[]; + if (fieldsetChanges.rulesets) { + payload.rulesets = fieldsetChanges.rulesets; } dispatch(updateFieldsetAction(payload)); }; - const getRuleValuePlaceholder = (ruleType: EFieldsetRuleType) => - formatMessage({ id: FIELDSET_RULE_VALUE_PLACEHOLDER_BY_TYPE[ruleType] }); + + const isTitleError = + (fieldsetChanges.title !== undefined || Boolean(localFieldset.title)) && + Boolean(validateFieldsetTitle(localFieldset.title)); if (isLoading) { return ; @@ -402,7 +352,7 @@ const FieldsetDetails = ({ id="fieldset-title" minRows={1} className={styles['settings-title']} - value={detailFieldset.title} + value={localFieldset.title} disabled /> ) : ( @@ -411,9 +361,9 @@ const FieldsetDetails = ({ type="text" className={classnames( styles['settings-title'], - Boolean(validateFieldsetTitle(detailFieldset.title)) && styles['settings-title_error'], + isTitleError && styles['settings-title_error'], )} - value={detailFieldset.title} + value={localFieldset.title} placeholder={formatMessage({ id: 'fieldsets.settings.title-placeholder' })} onChange={handleSettingsTitleChange} /> @@ -437,14 +387,14 @@ const FieldsetDetails = ({ id="fieldset-description" minRows={3} className={styles['settings-description']} - value={detailFieldset.description} + value={localFieldset.description} disabled /> ) : (