diff --git a/passbook/admin/templates/administration/overview.html b/passbook/admin/templates/administration/overview.html
index bc586ce7c..1c4cd1a4f 100644
--- a/passbook/admin/templates/administration/overview.html
+++ b/passbook/admin/templates/administration/overview.html
@@ -120,7 +120,17 @@
- {{ version }}
+ {% if version >= version_latest %}
+
+ {% blocktrans with version=version %}
+ {{ version }} (Up-to-date!)
+ {% endblocktrans %}
+ {% else %}
+
+ {% blocktrans with version=version latest=version_latest %}
+ {{ version }} ({{ latest }} is available!)
+ {% endblocktrans %}
+ {% endif %}
diff --git a/passbook/admin/views/overview.py b/passbook/admin/views/overview.py
index 83d0ea009..5c049cf7d 100644
--- a/passbook/admin/views/overview.py
+++ b/passbook/admin/views/overview.py
@@ -1,7 +1,11 @@
"""passbook administration overview"""
+from functools import lru_cache
+
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
+from packaging.version import Version, parse
+from requests import RequestException, get
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
@@ -12,6 +16,19 @@ from passbook.root.celery import CELERY_APP
from passbook.stages.invitation.models import Invitation
+@lru_cache
+def latest_version() -> Version:
+ """Get latest release from GitHub, cached"""
+ try:
+ data = get(
+ "https://api.github.com/repos/beryju/passbook/releases/latest"
+ ).json()
+ tag_name = data.get("tag_name")
+ return parse(tag_name.split("/")[1])
+ except RequestException:
+ return parse("0.0.0")
+
+
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
@@ -33,7 +50,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs["stage_count"] = len(Stage.objects.all())
kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
- kwargs["version"] = __version__
+ kwargs["version"] = parse(__version__)
+ kwargs["version_latest"] = latest_version()
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["providers_without_application"] = Provider.objects.filter(
application=None
diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py
index 0ff9668b7..256cdb589 100644
--- a/passbook/api/v2/urls.py
+++ b/passbook/api/v2/urls.py
@@ -32,8 +32,8 @@ from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceView
from passbook.sources.oauth.api import OAuthSourceViewSet
from passbook.sources.saml.api import SAMLSourceViewSet
from passbook.stages.captcha.api import CaptchaStageViewSet
-from passbook.stages.dummy.api import DummyStageViewSet
from passbook.stages.consent.api import ConsentStageViewSet
+from passbook.stages.dummy.api import DummyStageViewSet
from passbook.stages.email.api import EmailStageViewSet
from passbook.stages.identification.api import IdentificationStageViewSet
from passbook.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
diff --git a/swagger.yaml b/swagger.yaml
index 8a11bd93f..c5833bff9 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -3275,6 +3275,133 @@ paths:
required: true
type: string
format: uuid
+ /stages/consent/:
+ get:
+ operationId: stages_consent_list
+ description: ConsentStage Viewset
+ parameters:
+ - name: ordering
+ in: query
+ description: Which field to use when ordering the results.
+ required: false
+ type: string
+ - name: search
+ in: query
+ description: A search term.
+ required: false
+ type: string
+ - name: limit
+ in: query
+ description: Number of results to return per page.
+ required: false
+ type: integer
+ - name: offset
+ in: query
+ description: The initial index from which to return the results.
+ required: false
+ type: integer
+ responses:
+ '200':
+ description: ''
+ schema:
+ required:
+ - count
+ - results
+ type: object
+ properties:
+ count:
+ type: integer
+ next:
+ type: string
+ format: uri
+ x-nullable: true
+ previous:
+ type: string
+ format: uri
+ x-nullable: true
+ results:
+ type: array
+ items:
+ $ref: '#/definitions/ConsentStage'
+ tags:
+ - stages
+ post:
+ operationId: stages_consent_create
+ description: ConsentStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ responses:
+ '201':
+ description: ''
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ tags:
+ - stages
+ parameters: []
+ /stages/consent/{stage_uuid}/:
+ get:
+ operationId: stages_consent_read
+ description: ConsentStage Viewset
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ tags:
+ - stages
+ put:
+ operationId: stages_consent_update
+ description: ConsentStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ tags:
+ - stages
+ patch:
+ operationId: stages_consent_partial_update
+ description: ConsentStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/ConsentStage'
+ tags:
+ - stages
+ delete:
+ operationId: stages_consent_delete
+ description: ConsentStage Viewset
+ parameters: []
+ responses:
+ '204':
+ description: ''
+ tags:
+ - stages
+ parameters:
+ - name: stage_uuid
+ in: path
+ description: A UUID string identifying this Consent Stage.
+ required: true
+ type: string
+ format: uuid
/stages/dummy/:
get:
operationId: stages_dummy_list
@@ -6052,6 +6179,20 @@ definitions:
description: Private key, acquired from https://www.google.com/recaptcha/intro/v3.html
type: string
minLength: 1
+ ConsentStage:
+ required:
+ - name
+ type: object
+ properties:
+ pk:
+ title: Stage uuid
+ type: string
+ format: uuid
+ readOnly: true
+ name:
+ title: Name
+ type: string
+ minLength: 1
DummyStage:
required:
- name