Merge branch 'master' into e2e
# Conflicts: # Pipfile.lock # docs/installation/docker-compose.md
This commit is contained in:
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.8.15-beta"
|
||||
__version__ = "0.9.0-pre2"
|
||||
|
||||
@ -16,11 +16,13 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -65,14 +67,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -15,8 +15,10 @@
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
{% include 'partials/pagination.html' %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
|
||||
@ -16,11 +16,13 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -67,14 +69,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Certificates.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Certificates.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -16,11 +16,13 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -69,15 +71,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flows.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flows.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -17,12 +17,14 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -64,14 +66,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Groups.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no group exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Groups.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no group exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -22,8 +22,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -33,8 +33,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -49,8 +49,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -65,8 +65,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -76,8 +76,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -92,8 +92,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -103,8 +103,8 @@
|
||||
</a>
|
||||
|
||||
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -114,8 +114,8 @@
|
||||
</a>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -125,8 +125,8 @@
|
||||
</div>
|
||||
|
||||
<div class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
|
||||
</div>
|
||||
</div>
|
||||
@ -141,8 +141,8 @@
|
||||
</div>
|
||||
|
||||
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
|
||||
<div class="pf-c-card__head">
|
||||
<div class="pf-c-card__head-main">
|
||||
<div class="pf-c-card__header">
|
||||
<div class="pf-c-card__header-main">
|
||||
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,28 +16,30 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -81,31 +83,33 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policies.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policies exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policies.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policies exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -16,12 +16,14 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -57,14 +59,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policy Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policy bindings exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Policy Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no policy bindings exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -17,29 +17,31 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -75,31 +77,33 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Property Mappings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Property Mappings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -18,28 +18,30 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -94,30 +96,32 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Providers.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Providers.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -18,28 +18,30 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -88,30 +90,32 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Sources.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no sources exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Sources.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no sources exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -18,28 +18,30 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -62,6 +64,8 @@
|
||||
<ul>
|
||||
{% for flow in stage.flow_set.all %}
|
||||
<li><a href="{% url 'passbook_admin:flow-update' pk=flow.pk %}">{{ flow.slug }}</a></li>
|
||||
{% empty %}
|
||||
<li>-</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
@ -82,31 +86,33 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stages.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no stages exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stages.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no stages exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<div class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
{% for type, name in types.items %}
|
||||
<li>
|
||||
<a class="pf-c-dropdown__menu-item"
|
||||
href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
|
||||
{{ name|verbose_name }}<br>
|
||||
<small>
|
||||
{{ name|doc }}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -16,12 +16,14 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-binding-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -84,14 +86,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flow-Stage Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Flow-Stage Bindings.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -17,12 +17,14 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}"
|
||||
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -57,14 +59,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Invitations.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Invitations.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -17,11 +17,13 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -83,14 +85,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stage Prompts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Stage Prompts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -15,11 +15,13 @@
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
@ -64,14 +66,16 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Users.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no users exist. How did you even get here.' %}
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Users.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no users exist. How did you even get here.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -35,10 +35,12 @@
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% block action %}{% endblock %}" />
|
||||
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Cancel" %}</a>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% block action %}{% endblock %}" />
|
||||
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{% block above_form %}
|
||||
<h1>
|
||||
{% blocktrans with type=form|form_verbose_name|title inst=form.instance %}
|
||||
Update {{ type }}: {{ inst }}
|
||||
Update {{ inst }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
@ -6,10 +6,10 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
# User = apps.get_model("passbook_core", "User")
|
||||
# We have to use a direct import here, otherwise we get an object manager error
|
||||
from passbook.core.models import User
|
||||
|
||||
pbadmin = User.objects.create(
|
||||
pbadmin, _ = User.objects.get_or_create(
|
||||
username="pbadmin", email="root@localhost", name="passbook Default Admin"
|
||||
)
|
||||
pbadmin.set_password("pbadmin") # noqa # nosec
|
||||
|
||||
@ -25,8 +25,8 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-page__header-nav">
|
||||
<nav class="pf-c-nav" aria-label="Nav">
|
||||
<ul class="pf-c-nav__horizontal-list ws-top-nav">
|
||||
<nav class="pf-c-nav pf-m-horizontal" aria-label="Nav">
|
||||
<ul class="pf-c-nav__list ws-top-nav">
|
||||
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_core:overview' %}"
|
||||
href="{% url 'passbook_core:overview' %}">{% trans 'Access' %}</a></li>
|
||||
{% if user.is_superuser %}
|
||||
|
||||
@ -43,17 +43,19 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">{% trans 'No Applications available.' %}</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans "Either no applications are defined, or you don't have access to any." %}
|
||||
<div class="pf-c-empty-state pf-m-full-height">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">{% trans 'No Applications available.' %}</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans "Either no applications are defined, or you don't have access to any." %}
|
||||
</div>
|
||||
{% if user.is_superuser %} {# todo: use guardian permissions instead #}
|
||||
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
|
||||
{% trans 'Create Application' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if user.is_superuser %} {# todo: use guardian permissions instead #}
|
||||
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
|
||||
{% trans 'Create Application' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% if field.field.widget|fieldtype == 'HiddenInput' %}
|
||||
{{ field }}
|
||||
{% else %}
|
||||
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
|
||||
{% if field.field.widget|fieldtype == 'RadioSelect' %}
|
||||
<label class="pf-c-form__label" {% if field.field.required %}class="required" {% endif %}
|
||||
@ -66,4 +69,5 @@
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@ -5,55 +5,72 @@
|
||||
{% for field in form %}
|
||||
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
|
||||
{% if field.field.widget|fieldtype == 'RadioSelect' %}
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% for c in field %}
|
||||
<div class="radio col-sm-10">
|
||||
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}>
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif field.field.widget|fieldtype == 'Select' %}
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
{{ field|css_class:"pf-c-form-control" }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-check">
|
||||
{{ field|css_class:"pf-c-check__input" }}
|
||||
<label class="pf-c-check__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ field.label }}</label>
|
||||
<div class="pf-c-form__group-control">
|
||||
{% for c in field %}
|
||||
<div class="radio col-sm-10">
|
||||
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}>
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
</div>
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'Select' %}
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
{{ field|css_class:"pf-c-form-control" }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-check">
|
||||
{{ field|css_class:"pf-c-check__input" }}
|
||||
<label class="pf-c-check__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ field.label }}</label>
|
||||
</div>
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="c-form__horizontal-group">
|
||||
{{ field|css_class:'pf-c-form-control' }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="c-form__horizontal-group">
|
||||
{{ field|css_class:'pf-c-form-control' }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
|
||||
@ -1,43 +1,43 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="pf-c-pagination">
|
||||
<div class="pf-c-pagination__total-items">
|
||||
<b>{{ page_obj.start_index }} - {{ page_obj.end_index }}</b>of
|
||||
<b>{{ page_obj.count }}</b>
|
||||
</div>
|
||||
{% with param=get_param|default:'page' %}
|
||||
<nav class="pf-c-pagination__nav" aria-label="Pagination">
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to first page"
|
||||
href="?{{ param }}=1">
|
||||
<i class="fas fa-angle-double-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to previous page"
|
||||
{% if page_obj.has_previous %}
|
||||
href="?{{ param }}={{ page_obj.previous_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<i class="fas fa-angle-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
<div class="pf-c-pagination__nav-page-select">
|
||||
<span>
|
||||
{% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}
|
||||
{{ current }} of {{ total }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
<div class="pf-c-toolbar__item pf-m-pagination">
|
||||
<div class="pf-c-pagination">
|
||||
<div class="pf-c-pagination__total-items">
|
||||
<b>{{ page_obj.start_index }} - {{ page_obj.end_index }}</b>of
|
||||
<b>{{ page_obj.count }}</b>
|
||||
</div>
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to next page"
|
||||
{% if page_obj.has_next %}
|
||||
href="?{{ param }}={{ page_obj.next_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to last page"
|
||||
href="?{{ param }}={{ page_obj.num_pages }}">
|
||||
<i class="fas fa-angle-double-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% with param=get_param|default:'page' %}
|
||||
<nav class="pf-c-pagination__nav" aria-label="Pagination">
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to first page" href="?{{ param }}=1">
|
||||
<i class="fas fa-angle-double-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to previous page"
|
||||
{% if page_obj.has_previous %}
|
||||
href="?{{ param }}={{ page_obj.previous_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<i class="fas fa-angle-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
<div class="pf-c-pagination__nav-page-select">
|
||||
<span>
|
||||
{% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}
|
||||
{{ current }} of {{ total }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
</div>
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to next page"
|
||||
{% if page_obj.has_next %}
|
||||
href="?{{ param }}={{ page_obj.next_page_number }}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}>
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to last page" href="?{{ param }}={{ page_obj.num_pages }}">
|
||||
<i class="fas fa-angle-double-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</nav>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -44,6 +44,9 @@ class FlowStageBindingForm(forms.ModelForm):
|
||||
"re_evaluate_policies",
|
||||
"order",
|
||||
]
|
||||
labels = {
|
||||
"re_evaluate_policies": _("Re-evaluate Policies"),
|
||||
}
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
}
|
||||
|
||||
50
passbook/flows/markers.py
Normal file
50
passbook/flows/markers.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Stage Markers"""
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.models import Stage
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
from passbook.policies.models import PolicyBinding
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from passbook.flows.planner import FlowPlan
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class StageMarker:
|
||||
"""Base stage marker class, no extra attributes, and has no special handler."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]:
|
||||
"""Process callback for this marker. This should be overridden by sub-classes.
|
||||
If a stage should be removed, return None."""
|
||||
return stage
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReevaluateMarker(StageMarker):
|
||||
"""Reevaluate Marker, forces stage's policies to be evaluated again."""
|
||||
|
||||
binding: PolicyBinding
|
||||
user: User
|
||||
|
||||
def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]:
|
||||
"""Re-evaluate policies bound to stage, and if they fail, remove from plan"""
|
||||
engine = PolicyEngine(self.binding, self.user)
|
||||
engine.use_cache = False
|
||||
engine.request.context = plan.context
|
||||
engine.build()
|
||||
result = engine.result
|
||||
if result.passing:
|
||||
return stage
|
||||
LOGGER.warning(
|
||||
"f(plan_inst)[re-eval marker]: stage failed re-evaluation",
|
||||
stage=stage,
|
||||
messages=result.messages,
|
||||
)
|
||||
return None
|
||||
@ -9,7 +9,8 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.models import Flow, Stage
|
||||
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
||||
from passbook.flows.models import Flow, FlowStageBinding, Stage
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -33,12 +34,39 @@ class FlowPlan:
|
||||
of all Stages that should be run."""
|
||||
|
||||
flow_pk: str
|
||||
|
||||
stages: List[Stage] = field(default_factory=list)
|
||||
context: Dict[str, Any] = field(default_factory=dict)
|
||||
markers: List[StageMarker] = field(default_factory=list)
|
||||
|
||||
def next(self) -> Stage:
|
||||
def next(self) -> Optional[Stage]:
|
||||
"""Return next pending stage from the bottom of the list"""
|
||||
return self.stages[0]
|
||||
if not self.has_stages:
|
||||
return None
|
||||
stage = self.stages[0]
|
||||
marker = self.markers[0]
|
||||
|
||||
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
|
||||
marked_stage = marker.process(self, stage)
|
||||
if not marked_stage:
|
||||
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
|
||||
self.stages.remove(stage)
|
||||
self.markers.remove(marker)
|
||||
if not self.has_stages:
|
||||
return None
|
||||
# pylint: disable=not-callable
|
||||
return self.next()
|
||||
return marked_stage
|
||||
|
||||
def pop(self):
|
||||
"""Pop next pending stage from bottom of list"""
|
||||
self.markers.pop(0)
|
||||
self.stages.pop(0)
|
||||
|
||||
@property
|
||||
def has_stages(self) -> bool:
|
||||
"""Check if there are any stages left in this plan"""
|
||||
return len(self.markers) + len(self.stages) > 0
|
||||
|
||||
|
||||
class FlowPlanner:
|
||||
@ -100,7 +128,8 @@ class FlowPlanner:
|
||||
request: HttpRequest,
|
||||
default_context: Optional[Dict[str, Any]],
|
||||
) -> FlowPlan:
|
||||
"""Actually build flow plan"""
|
||||
"""Build flow plan by checking each stage in their respective
|
||||
order and checking the applied policies"""
|
||||
start_time = time()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex)
|
||||
if default_context:
|
||||
@ -111,13 +140,24 @@ class FlowPlanner:
|
||||
.select_subclasses()
|
||||
.select_related()
|
||||
):
|
||||
binding = stage.flowstagebinding_set.get(flow__pk=self.flow.pk)
|
||||
binding: FlowStageBinding = stage.flowstagebinding_set.get(
|
||||
flow__pk=self.flow.pk
|
||||
)
|
||||
engine = PolicyEngine(binding, user, request)
|
||||
engine.request.context = plan.context
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
LOGGER.debug("f(plan): Stage passing", stage=stage, flow=self.flow)
|
||||
plan.stages.append(stage)
|
||||
marker = StageMarker()
|
||||
if binding.re_evaluate_policies:
|
||||
LOGGER.debug(
|
||||
"f(plan): Stage has re-evaluate marker",
|
||||
stage=stage,
|
||||
flow=self.flow,
|
||||
)
|
||||
marker = ReevaluateMarker(binding=binding, user=user)
|
||||
plan.markers.append(marker)
|
||||
end_time = time()
|
||||
LOGGER.debug(
|
||||
"f(plan): Finished building",
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
<li>
|
||||
<a href="https://beryju.github.io/passbook/">{% trans 'Documentation' %}</a>
|
||||
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
|
||||
</li>
|
||||
<!-- todo: load config.passbook.footer.links -->
|
||||
</ul>
|
||||
@ -120,6 +120,7 @@ const updateCard = (data) => {
|
||||
break;
|
||||
case "template":
|
||||
flowBody.innerHTML = data.body;
|
||||
checkAutofocus();
|
||||
updateMessages();
|
||||
loadFormCode();
|
||||
setFormSubmitHandlers();
|
||||
@ -138,6 +139,12 @@ const loadFormCode = () => {
|
||||
document.head.appendChild(newScript);
|
||||
});
|
||||
};
|
||||
const checkAutofocus = () => {
|
||||
const autofocusElement = document.querySelector("#flow-body [autofocus]");
|
||||
if (autofocusElement !== null) {
|
||||
autofocusElement.focus();
|
||||
}
|
||||
};
|
||||
const updateFormAction = (form) => {
|
||||
for (let index = 0; index < form.elements.length; index++) {
|
||||
const element = form.elements[index];
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""flow planner tests"""
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import reverse
|
||||
from django.test import RequestFactory, TestCase
|
||||
@ -8,14 +9,19 @@ from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
from passbook.policies.dummy.models import DummyPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.policies.types import PolicyResult
|
||||
from passbook.stages.dummy.models import DummyStage
|
||||
|
||||
POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False))
|
||||
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
|
||||
TIME_NOW_MOCK = MagicMock(return_value=3)
|
||||
|
||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
|
||||
|
||||
class TestFlowPlanner(TestCase):
|
||||
"""Test planner logic"""
|
||||
@ -40,7 +46,7 @@ class TestFlowPlanner(TestCase):
|
||||
planner.plan(request)
|
||||
|
||||
@patch(
|
||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK,
|
||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
||||
)
|
||||
def test_non_applicable_plan(self):
|
||||
"""Test that empty plan raises exception"""
|
||||
@ -103,3 +109,71 @@ class TestFlowPlanner(TestCase):
|
||||
planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user})
|
||||
key = cache_key(flow, user)
|
||||
self.assertTrue(cache.get(key) is not None)
|
||||
|
||||
def test_planner_marker_reevaluate(self):
|
||||
"""Test that the planner creates the proper marker"""
|
||||
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="dummy1"),
|
||||
order=0,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
|
||||
request = self.request_factory.get(
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
request.user = get_anonymous_user()
|
||||
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(request)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], ReevaluateMarker)
|
||||
|
||||
def test_planner_reevaluate_actual(self):
|
||||
"""Test planner with re-evaluate"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-default-context",
|
||||
slug="test-default-context",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||
|
||||
request = self.request_factory.get(
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware()
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
# Here we patch the dummy policy to evaluate to true so the stage is included
|
||||
with patch(
|
||||
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
|
||||
):
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(request)
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
|
||||
@ -3,16 +3,21 @@ from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from passbook.flows.markers import ReevaluateMarker, StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import FlowPlan
|
||||
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.policies.dummy.models import DummyPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.policies.types import PolicyResult
|
||||
from passbook.stages.dummy.models import DummyStage
|
||||
|
||||
POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False))
|
||||
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
|
||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
|
||||
|
||||
class TestFlowExecutor(TestCase):
|
||||
@ -29,7 +34,9 @@ class TestFlowExecutor(TestCase):
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
stage = DummyStage.objects.create(name="dummy")
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex + "a", stages=[stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -45,7 +52,7 @@ class TestFlowExecutor(TestCase):
|
||||
self.assertEqual(cancel_mock.call_count, 1)
|
||||
|
||||
@patch(
|
||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK,
|
||||
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
|
||||
)
|
||||
def test_invalid_non_applicable_flow(self):
|
||||
"""Tests that a non-applicable flow returns the correct error message"""
|
||||
@ -125,3 +132,197 @@ class TestFlowExecutor(TestCase):
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
self.assertEqual(len(plan.stages), 1)
|
||||
|
||||
def test_reevaluate_remove_last(self):
|
||||
"""Test planner with re-evaluate (last stage is removed)"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-default-context",
|
||||
slug="test-default-context",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||
|
||||
# Here we patch the dummy policy to evaluate to true so the stage is included
|
||||
with patch(
|
||||
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
|
||||
):
|
||||
|
||||
exec_url = reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
# First request, run the planner
|
||||
response = self.client.get(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
|
||||
# Second request, this passes the first dummy stage
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# third request, this should trigger the re-evaluate
|
||||
# We do this request without the patch, so the policy results in false
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("passbook_core:overview"))
|
||||
|
||||
def test_reevaluate_remove_middle(self):
|
||||
"""Test planner with re-evaluate (middle stage is removed)"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-default-context",
|
||||
slug="test-default-context",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding3 = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||
|
||||
# Here we patch the dummy policy to evaluate to true so the stage is included
|
||||
with patch(
|
||||
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
|
||||
):
|
||||
|
||||
exec_url = reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
# First request, run the planner
|
||||
response = self.client.get(exec_url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.stages[2], binding3.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
self.assertIsInstance(plan.markers[2], StageMarker)
|
||||
|
||||
# Second request, this passes the first dummy stage
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding2.stage)
|
||||
self.assertEqual(plan.stages[1], binding3.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], StageMarker)
|
||||
|
||||
# third request, this should trigger the re-evaluate
|
||||
# We do this request without the patch, so the policy results in false
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_text(response.content),
|
||||
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
||||
)
|
||||
|
||||
def test_reevaluate_remove_consecutive(self):
|
||||
"""Test planner with re-evaluate (consecutive stages are removed)"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-default-context",
|
||||
slug="test-default-context",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding3 = FlowStageBinding.objects.create(
|
||||
flow=flow,
|
||||
stage=DummyStage.objects.create(name="dummy3"),
|
||||
order=2,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding4 = FlowStageBinding.objects.create(
|
||||
flow=flow, stage=DummyStage.objects.create(name="dummy4"), order=2
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
|
||||
PolicyBinding.objects.create(policy=false_policy, target=binding3, order=0)
|
||||
|
||||
# Here we patch the dummy policy to evaluate to true so the stage is included
|
||||
with patch(
|
||||
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
|
||||
):
|
||||
|
||||
exec_url = reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
# First request, run the planner
|
||||
response = self.client.get(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("dummy1", force_text(response.content))
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.stages[2], binding3.stage)
|
||||
self.assertEqual(plan.stages[3], binding4.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
self.assertIsInstance(plan.markers[2], ReevaluateMarker)
|
||||
self.assertIsInstance(plan.markers[3], StageMarker)
|
||||
|
||||
# Second request, this passes the first dummy stage
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# third request, this should trigger the re-evaluate
|
||||
# A get request will evaluate the policies and this will return stage 4
|
||||
# but it won't save it, hence we cant' check the plan
|
||||
response = self.client.get(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("dummy4", force_text(response.content))
|
||||
|
||||
# fourth request, this confirms the last stage (dummy4)
|
||||
# We do this request without the patch, so the policy results in false
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_text(response.content),
|
||||
{"type": "redirect", "to": reverse("passbook_core:overview")},
|
||||
)
|
||||
|
||||
@ -26,7 +26,7 @@ class TestHelperView(TestCase):
|
||||
def test_default_view_invalid_plan(self):
|
||||
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
|
||||
flow = Flow.objects.filter(designation=FlowDesignation.INVALIDATION,).first()
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex + "aa", stages=[])
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex + "aa")
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
@ -86,6 +86,9 @@ class FlowExecutorView(View):
|
||||
current_stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
if not self.current_stage:
|
||||
LOGGER.debug("f(exec): no more stages, flow is done.")
|
||||
return self._flow_done()
|
||||
stage_cls = path_to_class(self.current_stage.type)
|
||||
self.current_stage_view = stage_cls(self)
|
||||
self.current_stage_view.args = self.args
|
||||
@ -98,6 +101,7 @@ class FlowExecutorView(View):
|
||||
LOGGER.debug(
|
||||
"f(exec): Passing GET",
|
||||
view_class=class_to_path(self.current_stage_view.__class__),
|
||||
stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
stage_response = self.current_stage_view.get(request, *args, **kwargs)
|
||||
@ -108,6 +112,7 @@ class FlowExecutorView(View):
|
||||
LOGGER.debug(
|
||||
"f(exec): Passing POST",
|
||||
view_class=class_to_path(self.current_stage_view.__class__),
|
||||
stage=self.current_stage,
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
stage_response = self.current_stage_view.post(request, *args, **kwargs)
|
||||
@ -133,7 +138,11 @@ class FlowExecutorView(View):
|
||||
stage_class=class_to_path(self.current_stage_view.__class__),
|
||||
flow_slug=self.flow.slug,
|
||||
)
|
||||
self.plan.stages.pop(0)
|
||||
# We call plan.next here to check for re-evaluate markers
|
||||
# this is important so we can save the result
|
||||
# and we don't have to re-evaluate the policies each request
|
||||
self.plan.next()
|
||||
self.plan.pop()
|
||||
self.request.session[SESSION_KEY_PLAN] = self.plan
|
||||
if self.plan.stages:
|
||||
LOGGER.debug(
|
||||
|
||||
@ -7,23 +7,8 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
||||
Expression using Python. See <a href="https://passbook.beryju.org/policies/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
<ul class="pf-c-list">
|
||||
<li><code>request.user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/property-mappings/reference/user-object/">Reference</a>)</li>
|
||||
<li><code>request.http_request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
|
||||
<li><code>request.obj</code>: Model the Policy is run against. </li>
|
||||
<li><code>pb_flow_plan</code>: Current Plan if Policy is called while a flow is active.</li>
|
||||
<li><code>pb_is_sso_flow</code>: Boolean which is true if request was initiated by authenticating through an external Provider.</li>
|
||||
<li><code>pb_is_group_member(user, group_name)</code>: Function which checks if <code>user</code> is member of a Group with Name <code>group_name</code>.</li>
|
||||
<li><code>pb_logger</code>: Standard Python Logger Object, which can be used to debug expressions.</li>
|
||||
<li><code>pb_client_ip</code>: Client's IP Address.</li>
|
||||
</ul>
|
||||
<p>Custom Filters:</p>
|
||||
<ul class="pf-c-list">
|
||||
<li><code>regex_match(regex)</code>: Checks if value matches <code>regex</code></li>
|
||||
<li><code>regex_replace(regex, repl)</code>: Replace string matched by <code>regex</code> with <code>repl</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -2,7 +2,6 @@ version: "3.5"
|
||||
|
||||
services:
|
||||
passbook_gatekeeper:
|
||||
container_name: gatekeeper
|
||||
image: beryju/passbook-gatekeeper:{{ version }}
|
||||
ports:
|
||||
- 4180:4180
|
||||
|
||||
@ -23,11 +23,9 @@ OAUTH2_PROVIDER = {
|
||||
# this is the list of available scopes
|
||||
"SCOPES": {
|
||||
"openid": "Access OpenID Userinfo",
|
||||
"openid:userinfo": "Access OpenID Userinfo",
|
||||
"email": "Access OpenID E-Mail",
|
||||
# 'write': 'Write scope',
|
||||
# 'groups': 'Access to your groups',
|
||||
"user:email": "GitHub Compatibility: User E-Mail",
|
||||
"userinfo": "Access OpenID Userinfo",
|
||||
"email": "Access OpenID Email",
|
||||
"user:email": "GitHub Compatibility: User Email",
|
||||
"read:org": "GitHub Compatibility: User Groups",
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.html import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.admin.fields import CodeMirrorWidget
|
||||
from passbook.core.expression import PropertyMappingEvaluator
|
||||
from passbook.flows.models import Flow, FlowDesignation
|
||||
from passbook.providers.saml.models import (
|
||||
@ -74,4 +76,13 @@ class SAMLPropertyMappingForm(forms.ModelForm):
|
||||
"name": forms.TextInput(),
|
||||
"saml_name": forms.TextInput(),
|
||||
"friendly_name": forms.TextInput(),
|
||||
"expression": CodeMirrorWidget(mode="python"),
|
||||
}
|
||||
help_texts = {
|
||||
"saml_name": mark_safe(
|
||||
_(
|
||||
"URN OID used by SAML. This is optional. "
|
||||
'<a href="https://www.rfc-editor.org/rfc/rfc2798.html#section-2">Reference</a>'
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
@ -7,12 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
||||
<ul class="pf-c-list">
|
||||
<li><code>user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/reference/property-mappings/user-object/">Reference</a>)</li>
|
||||
<li><code>request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
|
||||
<li><code>provider</code>: Passbook SAML Provider Object (<a href="https://github.com/BeryJu/passbook/blob/master/passbook/providers/saml/models.py#L16">Reference</a>) </li>
|
||||
</ul>
|
||||
Expression using Python. See <a href="https://passbook.beryju.org/property-mappings/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,7 +102,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
|
||||
"""SAML Handler for SSO/Redirect bindings, which are sent via GET"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(
|
||||
def get( # lgtm [py/similar-function]
|
||||
self, request: HttpRequest, application_slug: str
|
||||
) -> Optional[HttpResponse]:
|
||||
"""Handle REDIRECT bindings"""
|
||||
|
||||
@ -188,7 +188,7 @@ WSGI_APPLICATION = "passbook.root.wsgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django_prometheus.db.backends.postgresql",
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": CONFIG.y("postgresql.host"),
|
||||
"NAME": CONFIG.y("postgresql.name"),
|
||||
"USER": CONFIG.y("postgresql.user"),
|
||||
|
||||
@ -4,6 +4,7 @@ from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.admin.fields import CodeMirrorWidget
|
||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from passbook.core.expression import PropertyMappingEvaluator
|
||||
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
@ -68,4 +69,8 @@ class LDAPPropertyMappingForm(forms.ModelForm):
|
||||
"name": forms.TextInput(),
|
||||
"ldap_property": forms.TextInput(),
|
||||
"object_field": forms.TextInput(),
|
||||
"expression": CodeMirrorWidget(mode="python"),
|
||||
}
|
||||
help_texts = {
|
||||
"object_field": _("Field of the user object this value is written to.")
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ class LDAPSource(Source):
|
||||
|
||||
form = "passbook.sources.ldap.forms.LDAPSourceForm"
|
||||
|
||||
_connection: Optional[Connection]
|
||||
_connection: Optional[Connection] = None
|
||||
|
||||
@property
|
||||
def connection(self) -> Connection:
|
||||
|
||||
@ -7,10 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
||||
<ul class="pf-c-list">
|
||||
<li><code>ldap</code>: A Dictionary of all values retrieved from LDAP.</li>
|
||||
</ul>
|
||||
Expression using Python. See <a href="https://passbook.beryju.org/property-mappings/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -94,8 +94,6 @@ class OAuthClient(BaseOAuthClient):
|
||||
"oauth_callback": callback,
|
||||
"token": raw_token,
|
||||
}
|
||||
callback = request.build_absolute_uri(callback or request.path)
|
||||
callback = force_text(callback)
|
||||
try:
|
||||
response = self.session.request(
|
||||
"post",
|
||||
|
||||
@ -5,6 +5,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -35,7 +36,9 @@ class TestCaptchaStage(TestCase):
|
||||
|
||||
def test_valid(self):
|
||||
"""Test valid captcha"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
@ -4,6 +4,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -30,7 +31,9 @@ class TestConsentStage(TestCase):
|
||||
|
||||
def test_valid(self):
|
||||
"""Test valid consent"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
"""passbook multi-stage authentication engine"""
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
from passbook.flows.stage import StageView
|
||||
@ -10,3 +12,8 @@ class DummyStage(StageView):
|
||||
def post(self, request: HttpRequest):
|
||||
"""Just redirect to next stage"""
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["title"] = self.executor.current_stage.name
|
||||
return kwargs
|
||||
|
||||
@ -6,13 +6,13 @@ from passbook.stages.email.models import EmailStage
|
||||
|
||||
|
||||
class EmailStageSendForm(forms.Form):
|
||||
"""Form used when sending the e-mail to prevent multiple emails being sent"""
|
||||
"""Form used when sending the email to prevent multiple emails being sent"""
|
||||
|
||||
invalid = forms.CharField(widget=forms.HiddenInput, required=True)
|
||||
|
||||
|
||||
class EmailStageForm(forms.ModelForm):
|
||||
"""Form to create/edit E-Mail Stage"""
|
||||
"""Form to create/edit Email Stage"""
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -34,6 +34,7 @@ class EmailStageForm(forms.ModelForm):
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"host": forms.TextInput(),
|
||||
"subject": forms.TextInput(),
|
||||
"username": forms.TextInput(),
|
||||
"password": forms.TextInput(),
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ from passbook.flows.models import Stage
|
||||
|
||||
|
||||
class EmailTemplates(models.TextChoices):
|
||||
"""Templates used for rendering the E-Mail"""
|
||||
"""Templates used for rendering the Email"""
|
||||
|
||||
PASSWORD_RESET = (
|
||||
"stages/email/for_email/password_reset.html",
|
||||
|
||||
@ -22,7 +22,7 @@ QS_KEY_TOKEN = "token"
|
||||
|
||||
|
||||
class EmailStageView(FormView, StageView):
|
||||
"""E-Mail stage which sends E-Mail for verification"""
|
||||
"""Email stage which sends Email for verification"""
|
||||
|
||||
form_class = EmailStageSendForm
|
||||
template_name = "stages/email/waiting_message.html"
|
||||
@ -41,11 +41,14 @@ class EmailStageView(FormView, StageView):
|
||||
token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN])
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
|
||||
token.delete()
|
||||
messages.success(request, _("Successfully verified E-Mail."))
|
||||
messages.success(request, _("Successfully verified Email."))
|
||||
return self.executor.stage_ok()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def form_invalid(self, form: EmailStageSendForm) -> HttpResponse:
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
messages.error(self.request, _("No pending user."))
|
||||
return super().form_invalid(form)
|
||||
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
valid_delta = timedelta(
|
||||
minutes=self.executor.current_stage.token_expiry + 1
|
||||
|
||||
@ -27,7 +27,7 @@ def send_mails(stage: EmailStage, *messages: List[EmailMultiAlternatives]):
|
||||
)
|
||||
# pylint: disable=unused-argument
|
||||
def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
||||
"""Send E-Mail according to EmailStage parameters from background worker.
|
||||
"""Send Email according to EmailStage parameters from background worker.
|
||||
Automatically retries if message couldn't be sent."""
|
||||
stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
|
||||
backend = stage.backend
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{ url }}" target="_blank">{% trans 'Confirm Account' %}</a> </td>
|
||||
<td> <a href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Confirm Account' %}</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{ url }}" target="_blank">{% trans 'Reset Password' %}</a> </td>
|
||||
<td> <a href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Reset Password' %}</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -33,7 +33,7 @@
|
||||
</table>
|
||||
<p>
|
||||
{% blocktrans with expires=expires|naturaltime %}
|
||||
If you did not request a password change, please ignore this E-Mail. The link above is valid for {{ expires }}.
|
||||
If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<form method="POST" class="pf-c-form">
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Check your E-Mails for a password reset link.
|
||||
Check your Emails for a password reset link.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
@ -15,7 +15,7 @@
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Recovery E-Mail." %}</button>
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Recovery Email." %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@ -7,6 +7,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import Token, User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -34,7 +35,9 @@ class TestEmailStage(TestCase):
|
||||
|
||||
def test_rendering(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -48,7 +51,9 @@ class TestEmailStage(TestCase):
|
||||
|
||||
def test_without_user(self):
|
||||
"""Test without pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -61,7 +66,9 @@ class TestEmailStage(TestCase):
|
||||
|
||||
def test_pending_user(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -82,7 +89,9 @@ class TestEmailStage(TestCase):
|
||||
"""Test with token"""
|
||||
# Make sure token exists
|
||||
self.test_pending_user()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""passbook flows identification forms"""
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.core.validators import validate_email
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from structlog import get_logger
|
||||
@ -19,6 +20,9 @@ class IdentificationStageForm(forms.ModelForm):
|
||||
fields = ["name", "user_fields", "template", "enrollment_flow", "recovery_flow"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"user_fields": FilteredSelectMultiple(
|
||||
_("fields"), False, choices=UserFields.choices
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -35,8 +39,16 @@ class IdentificationForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.stage.user_fields == [UserFields.E_MAIL]:
|
||||
self.fields["uid_field"] = forms.EmailField()
|
||||
self.fields["uid_field"].label = human_list(
|
||||
[x.title() for x in self.stage.user_fields]
|
||||
label = human_list([x.title() for x in self.stage.user_fields])
|
||||
self.fields["uid_field"].label = label
|
||||
self.fields["uid_field"].widget.attrs.update(
|
||||
{
|
||||
"placeholder": _(label),
|
||||
"autofocus": "autofocus",
|
||||
# Autocomplete according to
|
||||
# https://www.chromium.org/developers/design-documents/form-styles-that-chromium-understands
|
||||
"autocomplete": "username",
|
||||
}
|
||||
)
|
||||
|
||||
def clean_uid_field(self):
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.7 on 2020-06-15 16:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0005_provider_flows"),
|
||||
("passbook_stages_identification", "0002_auto_20200530_2204"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="identificationstage",
|
||||
name="recovery_flow",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Optional recovery 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -48,7 +48,7 @@ class IdentificationStage(Stage):
|
||||
related_name="+",
|
||||
default=None,
|
||||
help_text=_(
|
||||
"Optional enrollment flow, which is linked at the bottom of the page."
|
||||
"Optional recovery flow, which is linked at the bottom of the page."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ class TestIdentificationStage(TestCase):
|
||||
)
|
||||
|
||||
def test_invalid_with_username(self):
|
||||
"""Test invalid with username (user exists but stage only allows e-mail)"""
|
||||
"""Test invalid with username (user exists but stage only allows email)"""
|
||||
form_data = {"uid_field": self.user.username}
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
@ -72,7 +72,7 @@ class TestIdentificationStage(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_invalid_with_invalid_email(self):
|
||||
"""Test with invalid e-mail (user doesn't exist) -> Will return to login form"""
|
||||
"""Test with invalid email (user doesn't exist) -> Will return to login form"""
|
||||
form_data = {"uid_field": self.user.email + "test"}
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
|
||||
@ -7,6 +7,7 @@ from passbook.stages.invitation.models import Invitation, InvitationStage
|
||||
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
INVITATION_TOKEN_KEY = "token"
|
||||
INVITATION_IN_EFFECT = "invitation_in_effect"
|
||||
|
||||
|
||||
class InvitationStageView(StageView):
|
||||
@ -23,4 +24,5 @@ class InvitationStageView(StageView):
|
||||
token = request.GET[INVITATION_TOKEN_KEY]
|
||||
invite: Invitation = get_object_or_404(Invitation, pk=token)
|
||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data
|
||||
self.executor.plan.context[INVITATION_IN_EFFECT] = True
|
||||
return self.executor.stage_ok()
|
||||
|
||||
@ -7,6 +7,7 @@ from django.utils.encoding import force_text
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -39,7 +40,9 @@ class TestUserLoginStage(TestCase):
|
||||
|
||||
def test_without_invitation_fail(self):
|
||||
"""Test without any invitation, continue_flow_without_invitation not set."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
@ -64,7 +67,9 @@ class TestUserLoginStage(TestCase):
|
||||
"""Test without any invitation, continue_flow_without_invitation is set."""
|
||||
self.stage.continue_flow_without_invitation = True
|
||||
self.stage.save()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
@ -90,7 +95,9 @@ class TestUserLoginStage(TestCase):
|
||||
|
||||
def test_with_invitation(self):
|
||||
"""Test with invitation, check data in session"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
@ -110,7 +110,7 @@ class EnableView(LoginRequiredMixin, FormView):
|
||||
self.static_device = StaticDevice(user=request.user, confirmed=False)
|
||||
self.static_device.save()
|
||||
# Create 9 tokens and save them
|
||||
# TODO: Send static tokens via E-Mail
|
||||
# TODO: Send static tokens via Email
|
||||
for _counter in range(0, 9):
|
||||
token = StaticToken(
|
||||
device=self.static_device, token=StaticToken.random_token()
|
||||
|
||||
@ -1,25 +1,31 @@
|
||||
"""passbook administration forms"""
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.lib.utils.reflection import path_to_class
|
||||
from passbook.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
def get_authentication_backends():
|
||||
"""Return all available authentication backends as tuple set"""
|
||||
for backend in settings.AUTHENTICATION_BACKENDS:
|
||||
klass = path_to_class(backend)
|
||||
yield backend, getattr(
|
||||
klass(), "name", "%s (%s)" % (klass.__name__, klass.__module__)
|
||||
)
|
||||
return [
|
||||
(
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
_("passbook-internal Userdatabase"),
|
||||
),
|
||||
(
|
||||
"passbook.sources.ldap.auth.LDAPBackend",
|
||||
_("passbook LDAP (Only needed when User-Sync is not enabled."),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PasswordForm(forms.Form):
|
||||
"""Password authentication form"""
|
||||
|
||||
username = forms.CharField(
|
||||
widget=forms.HiddenInput(attrs={"autocomplete": "username"}), required=False
|
||||
)
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
|
||||
@ -52,9 +52,20 @@ class PasswordStage(FormView, StageView):
|
||||
form_class = PasswordForm
|
||||
template_name = "stages/password/backend.html"
|
||||
|
||||
def get_form(self, form_class=None) -> PasswordForm:
|
||||
form = super().get_form(form_class=form_class)
|
||||
|
||||
# If there's a pending user, update the `username` field
|
||||
# this field is only used by password managers.
|
||||
# If there's no user set, an error is raised later.
|
||||
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
||||
pending_user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
form.fields["username"].initial = pending_user.username
|
||||
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["primary_action"] = _("Log in")
|
||||
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
|
||||
if recovery_flow.exists():
|
||||
kwargs["recovery_flow"] = recovery_flow.first()
|
||||
|
||||
@ -9,6 +9,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -43,7 +44,9 @@ class TestPasswordStage(TestCase):
|
||||
|
||||
def test_without_user(self):
|
||||
"""Test without user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -68,7 +71,9 @@ class TestPasswordStage(TestCase):
|
||||
designation=FlowDesignation.RECOVERY, slug="qewrqerqr"
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -83,7 +88,9 @@ class TestPasswordStage(TestCase):
|
||||
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and valid password"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -105,7 +112,9 @@ class TestPasswordStage(TestCase):
|
||||
|
||||
def test_invalid_password(self):
|
||||
"""Test with a valid pending user and invalid password"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -127,7 +136,9 @@ class TestPasswordStage(TestCase):
|
||||
def test_permission_denied(self):
|
||||
"""Test with a valid pending user and valid password.
|
||||
Backend is patched to return PermissionError"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
33
passbook/stages/prompt/migrations/0003_auto_20200615_1641.py
Normal file
33
passbook/stages/prompt/migrations/0003_auto_20200615_1641.py
Normal file
@ -0,0 +1,33 @@
|
||||
# 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
33
passbook/stages/prompt/migrations/0004_auto_20200618_1735.py
Normal file
33
passbook/stages/prompt/migrations/0004_auto_20200618_1735.py
Normal file
@ -0,0 +1,33 @@
|
||||
# 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -12,8 +12,11 @@ from passbook.policies.models import PolicyBindingModel
|
||||
class FieldTypes(models.TextChoices):
|
||||
"""Field types an Prompt can be"""
|
||||
|
||||
# Simple text field
|
||||
TEXT = "text"
|
||||
EMAIL = "e-mail"
|
||||
# Same as text, but has autocomplete for password managers
|
||||
USERNAME = "username"
|
||||
EMAIL = "email"
|
||||
PASSWORD = "password" # noqa # nosec
|
||||
NUMBER = "number"
|
||||
CHECKBOX = "checkbox"
|
||||
@ -52,8 +55,11 @@ class Prompt(models.Model):
|
||||
}
|
||||
if self.type == FieldTypes.EMAIL:
|
||||
field_class = forms.EmailField
|
||||
if self.type == FieldTypes.USERNAME:
|
||||
attrs["autocomplete"] = "username"
|
||||
if self.type == FieldTypes.PASSWORD:
|
||||
widget = forms.PasswordInput(attrs=attrs)
|
||||
attrs["autocomplete"] = "new-password"
|
||||
if self.type == FieldTypes.NUMBER:
|
||||
field_class = forms.IntegerField
|
||||
widget = forms.NumberInput(attrs=attrs)
|
||||
@ -64,6 +70,10 @@ class Prompt(models.Model):
|
||||
if self.type == FieldTypes.CHECKBOX:
|
||||
field_class = forms.CheckboxInput
|
||||
kwargs["required"] = False
|
||||
if self.type == FieldTypes.DATE:
|
||||
field_class = forms.DateInput
|
||||
if self.type == FieldTypes.DATE_TIME:
|
||||
field_class = forms.DateTimeInput
|
||||
|
||||
# TODO: Implement static
|
||||
# TODO: Implement separator
|
||||
|
||||
@ -6,6 +6,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -96,7 +97,9 @@ class TestPromptStage(TestCase):
|
||||
|
||||
def test_render(self):
|
||||
"""Test render of form, check if all prompts are rendered correctly"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -114,7 +117,9 @@ class TestPromptStage(TestCase):
|
||||
|
||||
def test_valid_form_with_policy(self) -> PromptForm:
|
||||
"""Test form validation"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
expr = "return request.context['password_prompt'] == request.context['password2_prompt']"
|
||||
expr_policy = ExpressionPolicy.objects.create(
|
||||
name="validate-form", expression=expr
|
||||
@ -126,7 +131,9 @@ class TestPromptStage(TestCase):
|
||||
|
||||
def test_invalid_form(self) -> PromptForm:
|
||||
"""Test form validation"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
expr = "False"
|
||||
expr_policy = ExpressionPolicy.objects.create(
|
||||
name="validate-form", expression=expr
|
||||
@ -138,7 +145,9 @@ class TestPromptStage(TestCase):
|
||||
|
||||
def test_valid_form_request(self):
|
||||
"""Test a request with valid form data"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
@ -4,6 +4,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -29,7 +30,9 @@ class TestUserDeleteStage(TestCase):
|
||||
|
||||
def test_no_user(self):
|
||||
"""Test without user set"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -47,7 +50,9 @@ class TestUserDeleteStage(TestCase):
|
||||
|
||||
def test_user_delete_get(self):
|
||||
"""Test Form render"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -62,7 +67,9 @@ class TestUserDeleteStage(TestCase):
|
||||
|
||||
def test_user_delete_post(self):
|
||||
"""Test User delete (actual)"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -4,6 +4,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -30,7 +31,9 @@ class TestUserLoginStage(TestCase):
|
||||
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
@ -53,7 +56,9 @@ class TestUserLoginStage(TestCase):
|
||||
|
||||
def test_without_user(self):
|
||||
"""Test a plan without any pending user, resulting in a denied"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -72,7 +77,9 @@ class TestUserLoginStage(TestCase):
|
||||
|
||||
def test_without_backend(self):
|
||||
"""Test a plan with pending user, without backend, resulting in a denied"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -4,6 +4,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -30,7 +31,9 @@ class TestUserLogoutStage(TestCase):
|
||||
|
||||
def test_valid_password(self):
|
||||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
@ -7,6 +7,7 @@ from django.test import Client, TestCase
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.markers import StageMarker
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
@ -37,7 +38,9 @@ class TestUserWriteStage(TestCase):
|
||||
for _ in range(8)
|
||||
)
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
"username": "test-user",
|
||||
"name": "name",
|
||||
@ -71,7 +74,9 @@ class TestUserWriteStage(TestCase):
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
)
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create(
|
||||
username="unittest", email="test@beryju.org"
|
||||
)
|
||||
@ -104,7 +109,9 @@ class TestUserWriteStage(TestCase):
|
||||
|
||||
def test_without_data(self):
|
||||
"""Test without data results in error"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||
plan = FlowPlan(
|
||||
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
|
||||
)
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@patternfly/patternfly": "^2.71.6",
|
||||
"@fortawesome/fontawesome-free": "^5.13.1",
|
||||
"@patternfly/patternfly": "^4.10.31",
|
||||
"codemirror": "^5.54.0"
|
||||
}
|
||||
}
|
||||
|
||||
2
passbook/static/static/passbook/brand_inverted.svg
Normal file
2
passbook/static/static/passbook/brand_inverted.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="120px" height="20px" viewBox="15 0 10 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#000;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@ -198,120 +198,6 @@ input[data-is-monospace] {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
.ws-page-header {
|
||||
background-color: #151515;
|
||||
min-height: auto
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.ws-page-header .pf-c-page__header-nav {
|
||||
margin-left:12px
|
||||
}
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-nav__scroll-button {
|
||||
outline-offset: -4px;
|
||||
height: 100%;
|
||||
top: 0
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__item {
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link {
|
||||
padding-top: 22px;
|
||||
padding-right: var(--pf-global--spacer--md);
|
||||
padding-left: var(--pf-global--spacer--md);
|
||||
color: var(--pf-global--Color--light-100)
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link {
|
||||
padding-top:10px
|
||||
}
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:after {
|
||||
top: 0!important;
|
||||
height: 4px
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:active,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:hover {
|
||||
-webkit-transition: .5s;
|
||||
transition: .5s
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link.pf-m-current,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:active,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:hover {
|
||||
background-color: var(--pf-global--BackgroundColor--light-100);
|
||||
color: #151515!important;
|
||||
font-weight: var(--pf-global--FontWeight--normal)
|
||||
}
|
||||
|
||||
.ws-page-header li a:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%!important;
|
||||
bottom: 0;
|
||||
-webkit-transform: translateX(-50%) scaleX(0);
|
||||
transform: translateX(-50%) scaleX(0);
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
transform-origin: 50% 50%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--pf-global--BackgroundColor--light-100);
|
||||
color: #151515!important;
|
||||
-webkit-transition: -webkit-transform .25s;
|
||||
transition: -webkit-transform .25s;
|
||||
transition: transform .25s;
|
||||
transition: transform .25s,-webkit-transform .25s
|
||||
}
|
||||
|
||||
.ws-page-header li a:hover:after {
|
||||
-webkit-transform: translateX(-50%) scaleX(1);
|
||||
transform: translateX(-50%) scaleX(1)
|
||||
}
|
||||
|
||||
.ws-page-header li a.pf-m-current:after {
|
||||
left: 0!important;
|
||||
-webkit-transform: none;
|
||||
transform: none
|
||||
}
|
||||
|
||||
.ws-page-sidebar#page-sidebar {
|
||||
color: #fff;
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.ws-page-sidebar .pf-c-nav {
|
||||
margin-top: 16px
|
||||
}
|
||||
|
||||
.pf-site-search {
|
||||
padding: 0 0 2px;
|
||||
width: 150px;
|
||||
background: transparent;
|
||||
-webkit-transition: .25s;
|
||||
transition: .25s
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-page__header-brand-toggle {
|
||||
display: none;
|
||||
visibility: hidden
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-site-search {
|
||||
width:100px
|
||||
}
|
||||
|
||||
.ws-page-header .pf-c-page__header-brand-toggle {
|
||||
display: block;
|
||||
visibility: visible
|
||||
}
|
||||
}
|
||||
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
display: flex;
|
||||
|
||||
@ -2,15 +2,15 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@fortawesome/fontawesome-free@^5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9"
|
||||
integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==
|
||||
"@fortawesome/fontawesome-free@^5.13.1":
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz#c53b4066edae16cd1fd669f687baf031b45fb9d6"
|
||||
integrity sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw==
|
||||
|
||||
"@patternfly/patternfly@^2.71.6":
|
||||
version "2.71.6"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-2.71.6.tgz#6385cbd5aaca2f59bf65496e0189c541a7f00a82"
|
||||
integrity sha512-mqqtuCVa+/FbyyK8hSAcfEIwNX73+zbnzHpmC4NrW0kyMzSszPtBqev/ZO79ZxGqZUpLOyUBTVaH7oKn8cL35Q==
|
||||
"@patternfly/patternfly@^4.10.31":
|
||||
version "4.10.31"
|
||||
resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.10.31.tgz#742852b69d90bb2efe304130f7226d2e356306cf"
|
||||
integrity sha512-UxdZ/apWRowXYZ5qPz5LPfXwyB4YGpomrCJPX7c36+Zg8jFpYyVqgVYainL8Yf/GrChtC2LKyoHg7UUTtMtp4A==
|
||||
|
||||
codemirror@^5.54.0:
|
||||
version "5.54.0"
|
||||
|
||||
Reference in New Issue
Block a user