diff --git a/passbook/stages/password/migrations/0002_passwordstage_change_flow.py b/passbook/stages/password/migrations/0002_passwordstage_change_flow.py index 2c6e3257a..482e54428 100644 --- a/passbook/stages/password/migrations/0002_passwordstage_change_flow.py +++ b/passbook/stages/password/migrations/0002_passwordstage_change_flow.py @@ -16,8 +16,6 @@ def create_default_password_change(apps: Apps, schema_editor: BaseDatabaseSchema Flow = apps.get_model("passbook_flows", "Flow") FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding") - PolicyBinding = apps.get_model("passbook_policies", "PolicyBinding") - ExpressionPolicy = apps.get_model( "passbook_policies_expression", "ExpressionPolicy" ) @@ -58,17 +56,17 @@ def create_default_password_change(apps: Apps, schema_editor: BaseDatabaseSchema "order": 1, }, ) - prompt_stage.fields.add(password_prompt) - prompt_stage.fields.add(password_rep_prompt) # Policy to only trigger prompt when no username is given prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( name="default-password-change-password-equal", defaults={"expression": PROMPT_POLICY_EXPRESSION}, ) - PolicyBinding.objects.using(db_alias).update_or_create( - policy=prompt_policy, target=prompt_stage, defaults={"order": 0} - ) + + prompt_stage.fields.add(password_prompt) + prompt_stage.fields.add(password_rep_prompt) + prompt_stage.validation_policies.add(prompt_policy) + prompt_stage.save() user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create( name="default-password-change-write" @@ -103,9 +101,8 @@ class Migration(migrations.Migration): dependencies = [ ("passbook_flows", "0006_auto_20200629_0857"), ("passbook_policies_expression", "0001_initial"), - ("passbook_policies", "0001_initial"), ("passbook_stages_password", "0001_initial"), - ("passbook_stages_prompt", "0004_auto_20200618_1735"), + ("passbook_stages_prompt", "0001_initial"), ("passbook_stages_user_write", "0001_initial"), ] diff --git a/passbook/stages/prompt/api.py b/passbook/stages/prompt/api.py index b54d94d4c..a27d92b61 100644 --- a/passbook/stages/prompt/api.py +++ b/passbook/stages/prompt/api.py @@ -18,6 +18,7 @@ class PromptStageSerializer(ModelSerializer): "pk", "name", "fields", + "validation_policies", ] diff --git a/passbook/stages/prompt/forms.py b/passbook/stages/prompt/forms.py index 908395079..154c7fdf6 100644 --- a/passbook/stages/prompt/forms.py +++ b/passbook/stages/prompt/forms.py @@ -1,14 +1,17 @@ """Prompt forms""" -from typing import Callable +from email.policy import Policy +from typing import Callable, Iterator, List from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple +from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from guardian.shortcuts import get_anonymous_user from passbook.core.models import User from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.policies.engine import PolicyEngine +from passbook.policies.models import PolicyBinding, PolicyBindingModel from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage @@ -18,7 +21,7 @@ class PromptStageForm(forms.ModelForm): class Meta: model = PromptStage - fields = ["name", "fields"] + fields = ["name", "fields", "validation_policies"] widgets = { "name": forms.TextInput(), "fields": FilteredSelectMultiple(_("prompts"), False), @@ -45,6 +48,23 @@ class PromptAdminForm(forms.ModelForm): } +class ListPolicyEngine(PolicyEngine): + """Slightly modified policy engine, which uses a list instead of a PolicyBindingModel""" + + __list: List[Policy] + + def __init__( + self, policies: List[Policy], user: User, request: HttpRequest = None + ) -> None: + super().__init__(PolicyBindingModel(), user, request) + self.__list = policies + self.use_cache = False + + def _iter_bindings(self) -> Iterator[PolicyBinding]: + for policy in self.__list: + yield PolicyBinding(policy=policy,) + + class PromptForm(forms.Form): """Dynamically created form based on PromptStage""" @@ -73,7 +93,7 @@ class PromptForm(forms.Form): def clean(self): cleaned_data = super().clean() user = self.plan.context.get(PLAN_CONTEXT_PENDING_USER, get_anonymous_user()) - engine = PolicyEngine(self.stage, user) + engine = ListPolicyEngine(self.stage.validation_policies.all(), user) engine.request.context = cleaned_data engine.build() result = engine.result diff --git a/passbook/stages/prompt/migrations/0001_initial.py b/passbook/stages/prompt/migrations/0001_initial.py index 3b5e8aa00..7324bcf17 100644 --- a/passbook/stages/prompt/migrations/0001_initial.py +++ b/passbook/stages/prompt/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.6 on 2020-05-19 22:08 +# Generated by Django 3.1.1 on 2020-09-09 08:40 import uuid @@ -11,8 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("passbook_policies", "0001_initial"), - ("passbook_flows", "0001_initial"), + ("passbook_flows", "0007_auto_20200703_2059"), + ("passbook_policies", "0003_auto_20200908_1542"), ] operations = [ @@ -39,17 +39,30 @@ class Migration(migrations.Migration): "type", models.CharField( choices=[ - ("text", "Text"), - ("e-mail", "Email"), + ("text", "Text: Simple Text input"), + ( + "username", + "Username: Same as Text input, but checks for and prevents duplicate usernames.", + ), + ("email", "Email: Text field with Email type."), ("password", "Password"), ("number", "Number"), - ("hidden", "Hidden"), + ("checkbox", "Checkbox"), + ("data", "Date"), + ("data-time", "Date Time"), + ("separator", "Separator: Static Separator Line"), + ( + "hidden", + "Hidden: Hidden field, can be used to insert data into form.", + ), + ("static", "Static: Static value, displayed as-is."), ], max_length=100, ), ), ("required", models.BooleanField(default=True)), - ("placeholder", models.TextField()), + ("placeholder", models.TextField(blank=True)), + ("order", models.IntegerField(default=0)), ], options={"verbose_name": "Prompt", "verbose_name_plural": "Prompts",}, ), @@ -58,30 +71,25 @@ class Migration(migrations.Migration): fields=[ ( "stage_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - to="passbook_flows.Stage", - ), - ), - ( - "policybindingmodel_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to="passbook_policies.PolicyBindingModel", + to="passbook_flows.stage", ), ), ("fields", models.ManyToManyField(to="passbook_stages_prompt.Prompt")), + ( + "validation_policies", + models.ManyToManyField(blank=True, to="passbook_policies.Policy"), + ), ], options={ "verbose_name": "Prompt Stage", "verbose_name_plural": "Prompt Stages", }, - bases=("passbook_policies.policybindingmodel", "passbook_flows.stage"), + bases=("passbook_flows.stage",), ), ] diff --git a/passbook/stages/prompt/migrations/0002_auto_20200528_2059.py b/passbook/stages/prompt/migrations/0002_auto_20200528_2059.py deleted file mode 100644 index 5ecb06b4b..000000000 --- a/passbook/stages/prompt/migrations/0002_auto_20200528_2059.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 3.0.6 on 2020-05-28 20:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_stages_prompt", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="prompt", name="order", field=models.IntegerField(default=0), - ), - migrations.AlterField( - model_name="prompt", - name="type", - field=models.CharField( - choices=[ - ("text", "Text"), - ("e-mail", "Email"), - ("password", "Password"), - ("number", "Number"), - ("checkbox", "Checkbox"), - ("data", "Date"), - ("data-time", "Date Time"), - ("separator", "Separator"), - ("hidden", "Hidden"), - ("static", "Static"), - ], - max_length=100, - ), - ), - ] diff --git a/passbook/stages/prompt/migrations/0003_auto_20200615_1641.py b/passbook/stages/prompt/migrations/0003_auto_20200615_1641.py deleted file mode 100644 index 6ba175cc7..000000000 --- a/passbook/stages/prompt/migrations/0003_auto_20200615_1641.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.7 on 2020-06-15 16:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_stages_prompt", "0002_auto_20200528_2059"), - ] - - operations = [ - migrations.AlterField( - model_name="prompt", - name="type", - field=models.CharField( - choices=[ - ("text", "Text"), - ("username", "Username"), - ("e-mail", "Email"), - ("password", "Password"), - ("number", "Number"), - ("checkbox", "Checkbox"), - ("data", "Date"), - ("data-time", "Date Time"), - ("separator", "Separator"), - ("hidden", "Hidden"), - ("static", "Static"), - ], - max_length=100, - ), - ), - ] diff --git a/passbook/stages/prompt/migrations/0004_auto_20200618_1735.py b/passbook/stages/prompt/migrations/0004_auto_20200618_1735.py deleted file mode 100644 index 6e12574a0..000000000 --- a/passbook/stages/prompt/migrations/0004_auto_20200618_1735.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.7 on 2020-06-18 17:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_stages_prompt", "0003_auto_20200615_1641"), - ] - - operations = [ - migrations.AlterField( - model_name="prompt", - name="type", - field=models.CharField( - choices=[ - ("text", "Text"), - ("username", "Username"), - ("email", "Email"), - ("password", "Password"), - ("number", "Number"), - ("checkbox", "Checkbox"), - ("data", "Date"), - ("data-time", "Date Time"), - ("separator", "Separator"), - ("hidden", "Hidden"), - ("static", "Static"), - ], - max_length=100, - ), - ), - ] diff --git a/passbook/stages/prompt/migrations/0005_auto_20200709_1608.py b/passbook/stages/prompt/migrations/0005_auto_20200709_1608.py deleted file mode 100644 index 35c61d89a..000000000 --- a/passbook/stages/prompt/migrations/0005_auto_20200709_1608.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-09 16:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_stages_prompt", "0004_auto_20200618_1735"), - ] - - operations = [ - migrations.AlterField( - model_name="prompt", - name="type", - field=models.CharField( - choices=[ - ("text", "Text: Simple Text input"), - ( - "username", - "Username: Same as Text input, but checks for and prevents duplicate usernames.", - ), - ("email", "Email: Text field with Email type."), - ("password", "Password"), - ("number", "Number"), - ("checkbox", "Checkbox"), - ("data", "Date"), - ("data-time", "Date Time"), - ("separator", "Separator: Static Separator Line"), - ( - "hidden", - "Hidden: Hidden field, can be used to insert data into form.", - ), - ("static", "Static: Static value, displayed as-is."), - ], - max_length=100, - ), - ), - ] diff --git a/passbook/stages/prompt/migrations/0006_auto_20200823_2246.py b/passbook/stages/prompt/migrations/0006_auto_20200823_2246.py deleted file mode 100644 index eacb5e2d8..000000000 --- a/passbook/stages/prompt/migrations/0006_auto_20200823_2246.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 3.1 on 2020-08-23 22:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_stages_prompt", "0005_auto_20200709_1608"), - ] - - operations = [ - migrations.AlterField( - model_name="prompt", name="placeholder", field=models.TextField(blank=True), - ), - ] diff --git a/passbook/stages/prompt/models.py b/passbook/stages/prompt/models.py index f7cf1742f..3b7fbdeaa 100644 --- a/passbook/stages/prompt/models.py +++ b/passbook/stages/prompt/models.py @@ -11,7 +11,7 @@ from rest_framework.serializers import BaseSerializer from passbook.flows.models import Stage from passbook.lib.models import SerializerModel -from passbook.policies.models import PolicyBindingModel +from passbook.policies.models import Policy from passbook.stages.prompt.widgets import HorizontalRuleWidget, StaticTextWidget @@ -123,11 +123,13 @@ class Prompt(SerializerModel): verbose_name_plural = _("Prompts") -class PromptStage(PolicyBindingModel, Stage): +class PromptStage(Stage): """Define arbitrary prompts for the user.""" fields = models.ManyToManyField(Prompt) + validation_policies = models.ManyToManyField(Policy, blank=True) + @property def serializer(self) -> BaseSerializer: from passbook.stages.prompt.api import PromptStageSerializer diff --git a/passbook/stages/prompt/tests.py b/passbook/stages/prompt/tests.py index b293a36c2..bc8e3dd2f 100644 --- a/passbook/stages/prompt/tests.py +++ b/passbook/stages/prompt/tests.py @@ -11,7 +11,6 @@ from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding from passbook.flows.planner import FlowPlan from passbook.flows.views import SESSION_KEY_PLAN from passbook.policies.expression.models import ExpressionPolicy -from passbook.policies.models import PolicyBinding from passbook.stages.prompt.forms import PromptForm from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT @@ -124,7 +123,8 @@ class TestPromptStage(TestCase): expr_policy = ExpressionPolicy.objects.create( name="validate-form", expression=expr ) - PolicyBinding.objects.create(policy=expr_policy, target=self.stage, order=0) + self.stage.validation_policies.set([expr_policy]) + self.stage.save() form = PromptForm(stage=self.stage, plan=plan, data=self.prompt_data) self.assertEqual(form.is_valid(), True) return form @@ -138,7 +138,8 @@ class TestPromptStage(TestCase): expr_policy = ExpressionPolicy.objects.create( name="validate-form", expression=expr ) - PolicyBinding.objects.create(policy=expr_policy, target=self.stage, order=0) + self.stage.validation_policies.set([expr_policy]) + self.stage.save() form = PromptForm(stage=self.stage, plan=plan, data=self.prompt_data) self.assertEqual(form.is_valid(), False) return form