diff --git a/Pipfile.lock b/Pipfile.lock
index 7cb97bd17..06a8d01b3 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -18,10 +18,10 @@
"default": {
"amqp": {
"hashes": [
- "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
- "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
+ "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b",
+ "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"
],
- "version": "==2.5.2"
+ "version": "==2.6.0"
},
"asgiref": {
"hashes": [
@@ -53,17 +53,18 @@
},
"boto3": {
"hashes": [
- "sha256:c774003dc13d6de74b5e19d2b84d625da4456e64bd97f44baa1fcf40d808d29a"
+ "sha256:26f8564b46d009b8f4c6470a6d6cde147b282a197339c7e31cbb0fe9fd9e5f5d",
+ "sha256:f59d0bd230ed3a4b932c5c4e497a0e0ff3c93b46b7e8cde54efb6fe10c8266ba"
],
"index": "pypi",
- "version": "==1.13.19"
+ "version": "==1.13.20"
},
"botocore": {
"hashes": [
- "sha256:5cb537e7a4cf2d59a2a8dfbbc8e14ec3bc5b640eb81a1bf3bb0523c0a75e6b1b",
- "sha256:7b8b1f082665c8670b9aa70143ee527c5d04939fe027a63ac5958359be20ccb0"
+ "sha256:990f3fc33dec746829740b1a9e1fe86183cdc96aedba6a632ccfcbae03e097cc",
+ "sha256:d4cc47ac989a7f1d2992ef7679fb423a7966f687becf623a291a555a2d7ce1c0"
],
- "version": "==1.16.19"
+ "version": "==1.16.20"
},
"celery": {
"hashes": [
@@ -363,11 +364,11 @@
},
"kombu": {
"hashes": [
- "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76",
- "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2"
+ "sha256:ab0afaa5388dd2979cbc439d3623b86a4f7a58d41f621096bef7767c37bc2505",
+ "sha256:aece08f48706743aaa1b9d607fee300559481eafcc5ee56451aa0ef867a3be07"
],
"index": "pypi",
- "version": "==4.6.8"
+ "version": "==4.6.9"
},
"ldap3": {
"hashes": [
@@ -687,10 +688,10 @@
},
"redis": {
"hashes": [
- "sha256:2ef11f489003f151777c064c5dbc6653dfb9f3eade159bcadc524619fddc2242",
- "sha256:6d65e84bc58091140081ee9d9c187aab0480097750fac44239307a3bdf0b1251"
+ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
+ "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
- "version": "==3.5.2"
+ "version": "==3.5.3"
},
"requests": {
"hashes": [
@@ -857,10 +858,10 @@
},
"autopep8": {
"hashes": [
- "sha256:152fd8fe47d02082be86e05001ec23d6f420086db56b17fc883f3f965fb34954"
+ "sha256:60fd8c4341bab59963dafd5d2a566e94f547e660b9b396f772afe67d8481dbf0"
],
"index": "pypi",
- "version": "==1.5.2"
+ "version": "==1.5.3"
},
"bandit": {
"hashes": [
@@ -970,10 +971,10 @@
},
"gitpython": {
"hashes": [
- "sha256:864a47472548f3ba716ca202e034c1900f197c0fb3a08f641c20c3cafd15ed94",
- "sha256:da3b2cf819974789da34f95ac218ef99f515a928685db141327c09b73dd69c09"
+ "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a",
+ "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"
],
- "version": "==3.1.2"
+ "version": "==3.1.3"
},
"isort": {
"hashes": [
diff --git a/passbook/core/models.py b/passbook/core/models.py
index 08ad322dc..13517ea0e 100644
--- a/passbook/core/models.py
+++ b/passbook/core/models.py
@@ -13,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
from guardian.mixins import GuardianUserMixin
from jinja2 import Undefined
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
-from jinja2.nativetypes import NativeEnvironment
from model_utils.managers import InheritanceManager
from structlog import get_logger
@@ -24,7 +23,6 @@ from passbook.lib.models import CreatedUpdatedModel
from passbook.policies.models import PolicyBindingModel
LOGGER = get_logger()
-NATIVE_ENVIRONMENT = NativeEnvironment()
def default_token_duration():
@@ -208,8 +206,11 @@ class PropertyMapping(models.Model):
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
) -> Any:
"""Evaluate `self.expression` using `**kwargs` as Context."""
+ from passbook.policies.expression.evaluator import Evaluator
+
+ evaluator = Evaluator()
try:
- expression = NATIVE_ENVIRONMENT.from_string(self.expression)
+ expression = evaluator.env.from_string(self.expression)
except TemplateSyntaxError as exc:
raise PropertyMappingExpressionException from exc
try:
@@ -221,8 +222,11 @@ class PropertyMapping(models.Model):
raise PropertyMappingExpressionException from exc
def save(self, *args, **kwargs):
+ from passbook.policies.expression.evaluator import Evaluator
+
+ evaluator = Evaluator()
try:
- NATIVE_ENVIRONMENT.from_string(self.expression)
+ evaluator.env.from_string(self.expression)
except TemplateSyntaxError as exc:
raise ValidationError("Expression Syntax Error") from exc
return super().save(*args, **kwargs)
diff --git a/passbook/core/templates/partials/form.html b/passbook/core/templates/partials/form.html
index 83a6e8ae3..efd249d08 100644
--- a/passbook/core/templates/partials/form.html
+++ b/passbook/core/templates/partials/form.html
@@ -2,6 +2,13 @@
{% load i18n %}
{% csrf_token %}
+{% if form.non_field_errors %}
+
+{% endif %}
{% for field in form %}
{% if field.field.widget|fieldtype == 'RadioSelect' %}
diff --git a/passbook/flows/tests/test_planner.py b/passbook/flows/tests/test_planner.py
index 25f2bd1a7..df0dc5a7e 100644
--- a/passbook/flows/tests/test_planner.py
+++ b/passbook/flows/tests/test_planner.py
@@ -1,13 +1,15 @@
"""flow planner tests"""
from unittest.mock import MagicMock, patch
+from django.core.cache import cache
from django.shortcuts import reverse
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
+from passbook.core.models import User
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
-from passbook.flows.planner import FlowPlanner
+from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from passbook.policies.types import PolicyResult
from passbook.stages.dummy.models import DummyStage
@@ -81,3 +83,24 @@ class TestFlowPlanner(TestCase):
self.assertEqual(
TIME_NOW_MOCK.call_count, 2
) # When taking from cache, time is not measured
+
+ def test_planner_default_context(self):
+ """Test planner with default_context"""
+ flow = Flow.objects.create(
+ name="test-default-context",
+ slug="test-default-context",
+ designation=FlowDesignation.AUTHENTICATION,
+ )
+ FlowStageBinding.objects.create(
+ flow=flow, stage=DummyStage.objects.create(name="dummy"), order=0
+ )
+
+ user = User.objects.create(username="test-user")
+ request = self.request_factory.get(
+ reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
+ )
+ request.user = user
+ planner = FlowPlanner(flow)
+ planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user})
+ key = cache_key(flow, user)
+ self.assertTrue(cache.get(key) is not None)
diff --git a/passbook/flows/views.py b/passbook/flows/views.py
index 37380a90c..f955106db 100644
--- a/passbook/flows/views.py
+++ b/passbook/flows/views.py
@@ -34,7 +34,7 @@ class FlowExecutorView(View):
def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug)
- self.flow = get_object_or_404(Flow, slug=flow_slug)
+ self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
"""When a flow is non-applicable check if user is on the correct domain"""
diff --git a/passbook/policies/expression/evaluator.py b/passbook/policies/expression/evaluator.py
index b2120bb74..01ed3b7ae 100644
--- a/passbook/policies/expression/evaluator.py
+++ b/passbook/policies/expression/evaluator.py
@@ -1,22 +1,21 @@
"""passbook expression policy evaluator"""
import re
-from typing import TYPE_CHECKING, Any, Dict, Optional
+from typing import Any, Dict, List, Optional
from django.core.exceptions import ValidationError
+from django.http import HttpRequest
from jinja2 import Undefined
from jinja2.exceptions import TemplateSyntaxError
from jinja2.nativetypes import NativeEnvironment
from requests import Session
from structlog import get_logger
+from passbook.core.models import User
from passbook.flows.planner import PLAN_CONTEXT_SSO
from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.http import get_client_ip
from passbook.policies.types import PolicyRequest, PolicyResult
-if TYPE_CHECKING:
- from passbook.core.models import User
-
LOGGER = get_logger()
@@ -25,12 +24,33 @@ class Evaluator:
_env: NativeEnvironment
+ _context: Dict[str, Any]
+ _messages: List[str]
+
def __init__(self):
- self._env = NativeEnvironment()
+ self._env = NativeEnvironment(
+ extensions=["jinja2.ext.do"],
+ trim_blocks=True,
+ lstrip_blocks=True,
+ line_statement_prefix=">",
+ )
# update passbook/policies/expression/templates/policy/expression/form.html
# update docs/policies/expression/index.md
self._env.filters["regex_match"] = Evaluator.jinja2_filter_regex_match
self._env.filters["regex_replace"] = Evaluator.jinja2_filter_regex_replace
+ self._env.globals["pb_message"] = self.jinja2_func_message
+ self._context = {
+ "pb_is_group_member": Evaluator.jinja2_func_is_group_member,
+ "pb_user_by": Evaluator.jinja2_func_user_by,
+ "pb_logger": get_logger(),
+ "requests": Session(),
+ }
+ self._messages = []
+
+ @property
+ def env(self) -> NativeEnvironment:
+ """Access to our custom NativeEnvironment"""
+ return self._env
@staticmethod
def jinja2_filter_regex_match(value: Any, regex: str) -> bool:
@@ -43,56 +63,69 @@ class Evaluator:
return re.sub(regex, repl, value)
@staticmethod
- def jinja2_func_is_group_member(user: "User", group_name: str) -> bool:
+ def jinja2_func_user_by(**filters) -> Optional[User]:
+ """Get user by filters"""
+ users = User.objects.filter(**filters)
+ if users:
+ return users.first()
+ return None
+
+ @staticmethod
+ def jinja2_func_is_group_member(user: User, group_name: str) -> bool:
"""Check if `user` is member of group with name `group_name`"""
return user.groups.filter(name=group_name).exists()
- def _get_expression_context(
- self, request: PolicyRequest, **kwargs
- ) -> Dict[str, Any]:
- """Return dictionary with additional global variables passed to expression"""
+ def jinja2_func_message(self, message: str):
+ """Wrapper to append to messages list, which is returned with PolicyResult"""
+ self._messages.append(message)
+
+ def set_policy_request(self, request: PolicyRequest):
+ """Update context based on policy request (if http request is given, update that too)"""
# update passbook/policies/expression/templates/policy/expression/form.html
# update docs/policies/expression/index.md
- kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
- kwargs["pb_logger"] = get_logger()
- kwargs["requests"] = Session()
- kwargs["pb_is_sso_flow"] = request.context.get(PLAN_CONTEXT_SSO, False)
+ self._context["pb_is_sso_flow"] = request.context.get(PLAN_CONTEXT_SSO, False)
+ self._context["request"] = request
if request.http_request:
- kwargs["pb_client_ip"] = (
- get_client_ip(request.http_request) or "255.255.255.255"
- )
- if SESSION_KEY_PLAN in request.http_request.session:
- kwargs["pb_flow_plan"] = request.http_request.session[SESSION_KEY_PLAN]
- return kwargs
+ self.set_http_request(request.http_request)
- def evaluate(self, expression_source: str, request: PolicyRequest) -> PolicyResult:
- """Parse and evaluate expression.
- If the Expression evaluates to a list with 2 items, the first is used as passing bool and
- the second as messages.
- If the Expression evaluates to a truthy-object, it is used as passing bool."""
+ def set_http_request(self, request: HttpRequest):
+ """Update context based on http request"""
+ # update passbook/policies/expression/templates/policy/expression/form.html
+ # update docs/policies/expression/index.md
+ self._context["pb_client_ip"] = (
+ get_client_ip(request.http_request) or "255.255.255.255"
+ )
+ self._context["request"] = request
+ if SESSION_KEY_PLAN in request.http_request.session:
+ self._context["pb_flow_plan"] = request.http_request.session[
+ SESSION_KEY_PLAN
+ ]
+
+ def evaluate(self, expression_source: str) -> PolicyResult:
+ """Parse and evaluate expression. Policy is expected to return a truthy object.
+ Messages can be added using 'do pb_message()'."""
try:
- expression = self._env.from_string(expression_source)
+ expression = self._env.from_string(expression_source.lstrip().rstrip())
except TemplateSyntaxError as exc:
return PolicyResult(False, str(exc))
try:
- result: Optional[Any] = expression.render(
- request=request, **self._get_expression_context(request)
- )
+ result: Optional[Any] = expression.render(self._context)
+ except Exception as exc: # pylint: disable=broad-except
+ LOGGER.warning("Expression error", exc=exc)
+ return PolicyResult(False, str(exc))
+ else:
+ policy_result = PolicyResult(False)
+ policy_result.messages = tuple(self._messages)
if isinstance(result, Undefined):
LOGGER.warning(
"Expression policy returned undefined",
src=expression_source,
- req=request,
+ req=self._context,
)
- return PolicyResult(False)
- if isinstance(result, (list, tuple)) and len(result) == 2:
- return PolicyResult(*result)
+ policy_result.passing = False
if result:
- return PolicyResult(bool(result))
- return PolicyResult(False)
- except Exception as exc: # pylint: disable=broad-except
- LOGGER.warning("Expression error", exc=exc)
- return PolicyResult(False, str(exc))
+ policy_result.passing = bool(result)
+ return policy_result
def validate(self, expression: str):
"""Validate expression's syntax, raise ValidationError if Syntax is invalid"""
@@ -100,4 +133,4 @@ class Evaluator:
self._env.from_string(expression)
return True
except TemplateSyntaxError as exc:
- raise ValidationError("Expression Syntax Error") from exc
+ raise ValidationError(f"Expression Syntax Error: {str(exc)}") from exc
diff --git a/passbook/policies/expression/models.py b/passbook/policies/expression/models.py
index edf9b629b..e43f17560 100644
--- a/passbook/policies/expression/models.py
+++ b/passbook/policies/expression/models.py
@@ -16,7 +16,9 @@ class ExpressionPolicy(Policy):
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
- return Evaluator().evaluate(self.expression, request)
+ evaluator = Evaluator()
+ evaluator.set_policy_request(request)
+ return evaluator.evaluate(self.expression)
def save(self, *args, **kwargs):
Evaluator().validate(self.expression)
diff --git a/passbook/policies/expression/tests/test_evaluator.py b/passbook/policies/expression/tests/test_evaluator.py
index ca22e86ec..e39cdfec9 100644
--- a/passbook/policies/expression/tests/test_evaluator.py
+++ b/passbook/policies/expression/tests/test_evaluator.py
@@ -17,13 +17,15 @@ class TestEvaluator(TestCase):
"""test simple value expression"""
template = "True"
evaluator = Evaluator()
- self.assertEqual(evaluator.evaluate(template, self.request).passing, True)
+ evaluator.set_policy_request(self.request)
+ self.assertEqual(evaluator.evaluate(template).passing, True)
def test_messages(self):
"""test expression with message return"""
- template = "False, 'some message'"
+ template = '{% do pb_message("some message") %}False'
evaluator = Evaluator()
- result = evaluator.evaluate(template, self.request)
+ evaluator.set_policy_request(self.request)
+ result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("some message",))
@@ -31,7 +33,8 @@ class TestEvaluator(TestCase):
"""test invalid syntax"""
template = "{%"
evaluator = Evaluator()
- result = evaluator.evaluate(template, self.request)
+ evaluator.set_policy_request(self.request)
+ result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("tag name expected",))
@@ -39,7 +42,8 @@ class TestEvaluator(TestCase):
"""test undefined result"""
template = "{{ foo.bar }}"
evaluator = Evaluator()
- result = evaluator.evaluate(template, self.request)
+ evaluator.set_policy_request(self.request)
+ result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("'foo' is undefined",))
diff --git a/passbook/policies/types.py b/passbook/policies/types.py
index 1fd65e63b..29bf02932 100644
--- a/passbook/policies/types.py
+++ b/passbook/policies/types.py
@@ -39,4 +39,6 @@ class PolicyResult:
self.messages = messages
def __str__(self):
- return f"
"
+ if self.messages:
+ return f"PolicyResult passing={self.passing} messages={self.messages}"
+ return f"PolicyResult passing={self.passing}"
diff --git a/passbook/stages/identification/api.py b/passbook/stages/identification/api.py
index bd56a0a61..f40c329e3 100644
--- a/passbook/stages/identification/api.py
+++ b/passbook/stages/identification/api.py
@@ -16,6 +16,8 @@ class IdentificationStageSerializer(ModelSerializer):
"name",
"user_fields",
"template",
+ "enrollment_flow",
+ "recovery_flow",
]
diff --git a/passbook/stages/identification/migrations/0002_auto_20200530_2204.py b/passbook/stages/identification/migrations/0002_auto_20200530_2204.py
new file mode 100644
index 000000000..8a554f1ee
--- /dev/null
+++ b/passbook/stages/identification/migrations/0002_auto_20200530_2204.py
@@ -0,0 +1,41 @@
+# Generated by Django 3.0.6 on 2020-05-30 22:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("passbook_flows", "0002_default_flows"),
+ ("passbook_stages_identification", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="identificationstage",
+ name="enrollment_flow",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ help_text="Optional enrollment flow, which is linked at the bottom of the page.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_DEFAULT,
+ related_name="+",
+ to="passbook_flows.Flow",
+ ),
+ ),
+ migrations.AddField(
+ model_name="identificationstage",
+ name="recovery_flow",
+ field=models.ForeignKey(
+ blank=True,
+ default=None,
+ help_text="Optional enrollment flow, which is linked at the bottom of the page.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_DEFAULT,
+ related_name="+",
+ to="passbook_flows.Flow",
+ ),
+ ),
+ ]
diff --git a/passbook/stages/identification/models.py b/passbook/stages/identification/models.py
index d43b6b15c..94bf0ec48 100644
--- a/passbook/stages/identification/models.py
+++ b/passbook/stages/identification/models.py
@@ -3,7 +3,7 @@ from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _
-from passbook.flows.models import Stage
+from passbook.flows.models import Flow, Stage
class UserFields(models.TextChoices):
@@ -29,6 +29,29 @@ class IdentificationStage(Stage):
)
template = models.TextField(choices=Templates.choices)
+ enrollment_flow = models.ForeignKey(
+ Flow,
+ on_delete=models.SET_DEFAULT,
+ null=True,
+ blank=True,
+ related_name="+",
+ default=None,
+ help_text=_(
+ "Optional enrollment flow, which is linked at the bottom of the page."
+ ),
+ )
+ recovery_flow = models.ForeignKey(
+ Flow,
+ on_delete=models.SET_DEFAULT,
+ null=True,
+ blank=True,
+ related_name="+",
+ default=None,
+ help_text=_(
+ "Optional enrollment flow, which is linked at the bottom of the page."
+ ),
+ )
+
type = "passbook.stages.identification.stage.IdentificationStageView"
form = "passbook.stages.identification.forms.IdentificationStageForm"
diff --git a/passbook/stages/identification/stage.py b/passbook/stages/identification/stage.py
index c6abe62b6..38189bf1b 100644
--- a/passbook/stages/identification/stage.py
+++ b/passbook/stages/identification/stage.py
@@ -10,7 +10,6 @@ from django.views.generic import FormView
from structlog import get_logger
from passbook.core.models import Source, User
-from passbook.flows.models import FlowDesignation
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.stage import StageView
from passbook.stages.identification.forms import IdentificationForm
@@ -34,18 +33,17 @@ class IdentificationStageView(FormView, StageView):
return [current_stage.template]
def get_context_data(self, **kwargs):
+ current_stage: IdentificationStage = self.executor.current_stage
# Check for related enrollment and recovery flow, add URL to view
- enrollment_flow = self.executor.flow.related_flow(FlowDesignation.ENROLLMENT)
- if enrollment_flow:
+ if current_stage.enrollment_flow:
kwargs["enroll_url"] = reverse(
"passbook_flows:flow-executor-shell",
- kwargs={"flow_slug": enrollment_flow.slug},
+ kwargs={"flow_slug": current_stage.enrollment_flow.slug},
)
- recovery_flow = self.executor.flow.related_flow(FlowDesignation.RECOVERY)
- if recovery_flow:
+ if current_stage.recovery_flow:
kwargs["recovery_url"] = reverse(
"passbook_flows:flow-executor-shell",
- kwargs={"flow_slug": recovery_flow.slug},
+ kwargs={"flow_slug": current_stage.recovery_flow.slug},
)
kwargs["primary_action"] = _("Log in")
diff --git a/passbook/stages/identification/templates/stages/identification/recovery.html b/passbook/stages/identification/templates/stages/identification/recovery.html
index 1dab0ae77..4c9d07c18 100644
--- a/passbook/stages/identification/templates/stages/identification/recovery.html
+++ b/passbook/stages/identification/templates/stages/identification/recovery.html
@@ -1,72 +1,29 @@
-{% extends 'base/skeleton.html' %}
-
-{% load static %}
{% load i18n %}
+{% load static %}
-{% block body %}
-
-
+
+
+ {% trans 'Trouble Logging In?' %}
+
+
+
+ {% block card %}
+
+ {% endblock %}
-{% include 'partials/messages.html' %}
-
-
-
-
-
-
- {% trans 'Trouble Logging In?' %}
-
-
-
- {% block card %}
-
- {% endblock %}
-
-
-
-
-
-
-{% endblock %}
+
diff --git a/passbook/stages/identification/tests.py b/passbook/stages/identification/tests.py
index 4ecf255e9..fa8c3919d 100644
--- a/passbook/stages/identification/tests.py
+++ b/passbook/stages/identification/tests.py
@@ -85,15 +85,19 @@ class TestIdentificationStage(TestCase):
slug="unique-enrollment-string",
designation=FlowDesignation.ENROLLMENT,
)
+ self.stage.enrollment_flow = flow
+ self.stage.save()
FlowStageBinding.objects.create(
flow=flow, stage=self.stage, order=0,
)
response = self.client.get(
- reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
+ reverse(
+ "passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
+ ),
)
self.assertEqual(response.status_code, 200)
- self.assertIn(flow.name, response.rendered_content)
+ self.assertIn(flow.slug, response.rendered_content)
def test_recovery_flow(self):
"""Test that recovery flow is linked correctly"""
@@ -102,12 +106,16 @@ class TestIdentificationStage(TestCase):
slug="unique-recovery-string",
designation=FlowDesignation.RECOVERY,
)
+ self.stage.recovery_flow = flow
+ self.stage.save()
FlowStageBinding.objects.create(
flow=flow, stage=self.stage, order=0,
)
response = self.client.get(
- reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
+ reverse(
+ "passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
+ ),
)
self.assertEqual(response.status_code, 200)
- self.assertIn(flow.name, response.rendered_content)
+ self.assertIn(flow.slug, response.rendered_content)
diff --git a/passbook/stages/prompt/forms.py b/passbook/stages/prompt/forms.py
index 7347e9053..e9157d198 100644
--- a/passbook/stages/prompt/forms.py
+++ b/passbook/stages/prompt/forms.py
@@ -64,4 +64,4 @@ class PromptForm(forms.Form):
engine.build()
result = engine.result
if not result.passing:
- raise forms.ValidationError(result.messages)
+ raise forms.ValidationError(list(result.messages))
diff --git a/swagger.yaml b/swagger.yaml
index d999299c8..ac38448f0 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -5919,6 +5919,20 @@ definitions:
enum:
- stages/identification/login.html
- stages/identification/recovery.html
+ enrollment_flow:
+ title: Enrollment flow
+ description: Optional enrollment flow, which is linked at the bottom of the
+ page.
+ type: string
+ format: uuid
+ x-nullable: true
+ recovery_flow:
+ title: Recovery flow
+ description: Optional enrollment flow, which is linked at the bottom of the
+ page.
+ type: string
+ format: uuid
+ x-nullable: true
InvitationStage:
required:
- name