forked from data.coop/membersystem
Services (#25)
Dette PR handler om at få en måde hvorpå medlemmer kan få adgang til de forskellige services vi udbyder. Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk> Reviewed-on: data.coop/membersystem#25 Reviewed-by: benjaoming <benjaoming@data.coop> Co-authored-by: Víðir Valberg Guðmundsson <valberg@orn.li> Co-committed-by: Víðir Valberg Guðmundsson <valberg@orn.li>
This commit is contained in:
parent
43d5dcbd52
commit
3dd7352d90
0
requirements/base.txt
Normal file
0
requirements/base.txt
Normal file
|
@ -20,6 +20,7 @@ from .emails import InviteEmail
|
||||||
from .models import Member
|
from .models import Member
|
||||||
from .models import Membership
|
from .models import Membership
|
||||||
from .models import MembershipType
|
from .models import MembershipType
|
||||||
|
from .models import ServiceAccess
|
||||||
from .models import SubscriptionPeriod
|
from .models import SubscriptionPeriod
|
||||||
from .models import WaitingListEntry
|
from .models import WaitingListEntry
|
||||||
|
|
||||||
|
@ -46,6 +47,12 @@ class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
||||||
"""Admin for SubscriptionPeriod model."""
|
"""Admin for SubscriptionPeriod model."""
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ServiceAccess)
|
||||||
|
class ServiceAccessAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for ServiceAccess model."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MembershipInlineAdmin(admin.TabularInline):
|
class MembershipInlineAdmin(admin.TabularInline):
|
||||||
"""Inline admin."""
|
"""Inline admin."""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Generated by Django 5.1rc1 on 2024-12-22 23:20
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_registries.registry
|
||||||
|
import services.registry
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('membership', '0010_waitinglistentry_member'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ServiceAccess',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('service', django_registries.registry.ChoicesField(choices=[('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service')),
|
||||||
|
('subscription_data', models.JSONField(blank=True, null=True, verbose_name='subscription data')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'service access',
|
||||||
|
'verbose_name_plural': 'service accesses',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='membership',
|
||||||
|
options={'verbose_name': 'membership', 'verbose_name_plural': 'memberships'},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='subscriptionperiod',
|
||||||
|
name='exclude_overlapping_periods',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='serviceaccess',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='serviceaccess',
|
||||||
|
constraint=models.UniqueConstraint(fields=('user', 'service'), name='unique_user_service'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -12,6 +12,7 @@ from django.contrib.postgres.fields import RangeOperators
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from services.registry import ServiceRegistry
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from utils.mixins import CreatedModifiedAbstract
|
from utils.mixins import CreatedModifiedAbstract
|
||||||
|
|
||||||
|
@ -91,14 +92,14 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
|
||||||
period = DateRangeField(verbose_name=_("period"))
|
period = DateRangeField(verbose_name=_("period"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints: ClassVar = [
|
constraints: (
|
||||||
ExclusionConstraint(
|
ExclusionConstraint(
|
||||||
name="exclude_overlapping_periods",
|
name="exclude_overlapping_periods",
|
||||||
expressions=[
|
expressions=[
|
||||||
("period", RangeOperators.OVERLAPS),
|
("period", RangeOperators.OVERLAPS),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
||||||
|
@ -245,3 +246,30 @@ class WaitingListEntry(CreatedModifiedAbstract):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("waiting list entry")
|
verbose_name = _("waiting list entry")
|
||||||
verbose_name_plural = _("waiting list entries")
|
verbose_name_plural = _("waiting list entries")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceAccess(CreatedModifiedAbstract):
|
||||||
|
"""Access to a service for a user."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("service access")
|
||||||
|
verbose_name_plural = _("service accesses")
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "service"],
|
||||||
|
name="unique_user_service",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
service = ServiceRegistry.choices_field(verbose_name=_("service"))
|
||||||
|
|
||||||
|
subscription_data = models.JSONField(
|
||||||
|
verbose_name=_("subscription data"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.user} - {self.service}"
|
||||||
|
|
|
@ -46,12 +46,15 @@ THIRD_PARTY_APPS = [
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
"django_view_decorator",
|
"django_view_decorator",
|
||||||
|
"django_registries",
|
||||||
|
"oauth2_provider",
|
||||||
]
|
]
|
||||||
|
|
||||||
LOCAL_APPS = [
|
LOCAL_APPS = [
|
||||||
"utils",
|
"utils",
|
||||||
"accounting",
|
"accounting",
|
||||||
"membership",
|
"membership",
|
||||||
|
"services",
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -160,6 +163,16 @@ ACCOUNT_EMAIL_REQUIRED = True
|
||||||
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
||||||
ACCOUNT_USERNAME_REQUIRED = False
|
ACCOUNT_USERNAME_REQUIRED = False
|
||||||
|
|
||||||
|
# OAuth2 configuration
|
||||||
|
OAUTH2_PROVIDER = {
|
||||||
|
"OIDC_ENABLED": True,
|
||||||
|
"SCOPES": {
|
||||||
|
"openid": "OpenID Connect scope",
|
||||||
|
"profile": "Profile Information",
|
||||||
|
},
|
||||||
|
"PKCE_REQUIRED": False, # this can be a callable - https://github.com/jazzband/django-oauth-toolkit/issues/711#issuecomment-497073038
|
||||||
|
}
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
# We want to log everything to stdout in docker
|
# We want to log everything to stdout in docker
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
|
|
|
@ -51,6 +51,7 @@ h6 {
|
||||||
--light-dust: #fefef9;
|
--light-dust: #fefef9;
|
||||||
--dust: #f4f1ef;
|
--dust: #f4f1ef;
|
||||||
--medium-dust: #dadada;
|
--medium-dust: #dadada;
|
||||||
|
--medium-dust : #dadada;
|
||||||
--dark-dust: #bfbfbf;
|
--dark-dust: #bfbfbf;
|
||||||
--fade: #878787;
|
--fade: #878787;
|
||||||
--twilight: #4a4a4a;
|
--twilight: #4a4a4a;
|
||||||
|
@ -256,7 +257,7 @@ div.content-view>h2 {
|
||||||
|
|
||||||
div.services {
|
div.services {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: start;
|
||||||
gap: var(--double-space);
|
gap: var(--double-space);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,13 +78,13 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% comment %}
|
{% if user.is_superuser %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/services" class="{% active_path "services" "current" %}">
|
<a href="{% url "services:list" %}" class="{% active_path "services:list" "current" %}">
|
||||||
Services
|
Services
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endcomment %}
|
{% endif %}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "account_email" %}" class="{% active_path "account_email" "current" %}">
|
<a href="{% url "account_email" %}" class="{% active_path "account_email" "current" %}">
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="content-view">
|
|
||||||
Coming soon!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
<div class="content-view">
|
|
||||||
<h2>Services you subscribe to</h2>
|
|
||||||
<div class="services">
|
|
||||||
<div>
|
|
||||||
<div class="description">
|
|
||||||
<h3>Passit</h3>
|
|
||||||
<p>Passit is a service that blabla</p>
|
|
||||||
<a href="#">Read more …</a>
|
|
||||||
</div>
|
|
||||||
<a>Unsubscribe</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-view">
|
|
||||||
<h2>Available services</h2>
|
|
||||||
<div class="services">
|
|
||||||
<div>
|
|
||||||
<div class="description">
|
|
||||||
<h3>Forgejo</h3>
|
|
||||||
<p>Forgejo is a service that blabla</p>
|
|
||||||
<a href="#">Read more …</a>
|
|
||||||
</div>
|
|
||||||
<a>Subscribe</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="description">
|
|
||||||
<h3>Mastodon</h3>
|
|
||||||
<p>Mastodon is a service where you can write things to people around the world.</p>
|
|
||||||
<a href="#">Read more …</a>
|
|
||||||
</div>
|
|
||||||
<a>Subscribe</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="description">
|
|
||||||
<h3>Matrix</h3>
|
|
||||||
<p>Matrix is a service that blabla</p>
|
|
||||||
<a href="#">Read more …</a>
|
|
||||||
</div>
|
|
||||||
<a>Subscribe</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="description">
|
|
||||||
<h3>NextCloud</h3>
|
|
||||||
<p>NextCloud is a service that blabla</p>
|
|
||||||
<a href="#">Read more …</a>
|
|
||||||
</div>
|
|
||||||
<a>Subscribe</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endcomment %}
|
|
||||||
{% endblock %}
|
|
|
@ -8,6 +8,7 @@ from django_view_decorator import include_view_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include_view_urls(extra_modules=["project.views"])),
|
path("", include_view_urls(extra_modules=["project.views"])),
|
||||||
|
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
path("_admin/", admin.site.urls),
|
path("_admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
"""Project views."""
|
"""Project views."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from membership.models import ServiceAccess
|
||||||
|
from services.registry import ServiceRegistry
|
||||||
|
from utils.view_utils import render
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from accounting.models import Order
|
from accounting.models import Order
|
||||||
from django_view_decorator import view
|
from django_view_decorator import view
|
||||||
from utils.view_utils import render
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
@ -25,13 +26,3 @@ def index(request: HttpRequest) -> HttpResponse:
|
||||||
context = {"unpaid_orders": list(unpaid_orders)}
|
context = {"unpaid_orders": list(unpaid_orders)}
|
||||||
|
|
||||||
return render(request, "index.html", context=context)
|
return render(request, "index.html", context=context)
|
||||||
|
|
||||||
|
|
||||||
@view(
|
|
||||||
paths="services/",
|
|
||||||
name="services",
|
|
||||||
login_required=True,
|
|
||||||
)
|
|
||||||
def services_overview(request: HttpRequest) -> HttpResponse:
|
|
||||||
"""View to show the services overview."""
|
|
||||||
return render(request, "services_overview.html")
|
|
||||||
|
|
0
src/services/__init__.py
Normal file
0
src/services/__init__.py
Normal file
1
src/services/admin.py
Normal file
1
src/services/admin.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Register your models here.
|
6
src/services/apps.py
Normal file
6
src/services/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "services"
|
0
src/services/migrations/__init__.py
Normal file
0
src/services/migrations/__init__.py
Normal file
1
src/services/models.py
Normal file
1
src/services/models.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Create your models here.
|
38
src/services/registry.py
Normal file
38
src/services/registry.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
"""Registry for services."""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django_registries.registry import Interface
|
||||||
|
from django_registries.registry import Registry
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceRegistry(Registry):
|
||||||
|
"""Registry for services."""
|
||||||
|
|
||||||
|
implementations_module = "services"
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceInterface(Interface):
|
||||||
|
"""Interface for services."""
|
||||||
|
|
||||||
|
registry = ServiceRegistry
|
||||||
|
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
public: bool = False
|
||||||
|
|
||||||
|
# TODO: add a way to add a something which defines the required fields for a service
|
||||||
|
# - maybe a list of tuples with the field name and the type of the field
|
||||||
|
# this could be used to generate a form for the service, and also to validate
|
||||||
|
# the data saved in a JSONField on the ServiceAccess model
|
||||||
|
|
||||||
|
subscribe_fields: tuple[tuple[str, forms.Field]] = []
|
||||||
|
|
||||||
|
def get_form_class(self) -> type:
|
||||||
|
"""Get the form class for the service."""
|
||||||
|
return type(
|
||||||
|
"ServiceForm",
|
||||||
|
(forms.Form,),
|
||||||
|
dict(self.subscribe_fields),
|
||||||
|
)
|
76
src/services/services.py
Normal file
76
src/services/services.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""Service classes for data.coop."""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .registry import ServiceInterface
|
||||||
|
|
||||||
|
|
||||||
|
class MailService(ServiceInterface):
|
||||||
|
"""Mail service."""
|
||||||
|
|
||||||
|
slug = "mail"
|
||||||
|
name = "Mail"
|
||||||
|
url = "https://mail.data.coop"
|
||||||
|
description = "Mail service for data.coop"
|
||||||
|
|
||||||
|
subscribe_fields = (("username", forms.CharField()),)
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixService(ServiceInterface):
|
||||||
|
"""Matrix service."""
|
||||||
|
|
||||||
|
slug = "matrix"
|
||||||
|
name = "Matrix"
|
||||||
|
url = "https://matrix.data.coop"
|
||||||
|
description = "Matrix service for data.coop"
|
||||||
|
|
||||||
|
subscribe_fields = (("username", forms.CharField()),)
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonService(ServiceInterface):
|
||||||
|
"""Mastodon service."""
|
||||||
|
|
||||||
|
slug = "mastodon"
|
||||||
|
name = "Mastodon"
|
||||||
|
url = "https://social.data.coop"
|
||||||
|
description = "Mastodon service for data.coop"
|
||||||
|
|
||||||
|
subscribe_fields = (("username", forms.CharField()),)
|
||||||
|
|
||||||
|
|
||||||
|
class NextcloudService(ServiceInterface):
|
||||||
|
"""Nextcloud service."""
|
||||||
|
|
||||||
|
slug = "nextcloud"
|
||||||
|
name = "Nextcloud"
|
||||||
|
url = "https://cloud.data.coop"
|
||||||
|
description = "Nextcloud service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class HedgeDocService(ServiceInterface):
|
||||||
|
"""HedgeDoc service."""
|
||||||
|
|
||||||
|
slug = "hedgedoc"
|
||||||
|
name = "HedgeDoc"
|
||||||
|
url = "https://pad.data.coop"
|
||||||
|
public = True
|
||||||
|
description = "HedgeDoc service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class ForgejoService(ServiceInterface):
|
||||||
|
"""Forgejo service."""
|
||||||
|
|
||||||
|
slug = "forgejo"
|
||||||
|
name = "Forgejo"
|
||||||
|
url = "https://git.data.coop"
|
||||||
|
description = "Git service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class RalllyService(ServiceInterface):
|
||||||
|
"""Rallly service."""
|
||||||
|
|
||||||
|
slug = "rallly"
|
||||||
|
name = "Rallly"
|
||||||
|
url = "https://when.data.coop"
|
||||||
|
public = True
|
||||||
|
description = "Rallly service for data.coop"
|
9
src/services/templates/services/service_detail.html
Normal file
9
src/services/templates/services/service_detail.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>{{ service.name }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
19
src/services/templates/services/service_subscribe.html
Normal file
19
src/services/templates/services/service_subscribe.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>Subscribe to {{ service.name }}</h2>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
45
src/services/templates/services/services_overview.html
Normal file
45
src/services/templates/services/services_overview.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>Services you subscribe to</h2>
|
||||||
|
<div class="services">
|
||||||
|
{% for service in active_services %}
|
||||||
|
<div>
|
||||||
|
<div class="description">
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
|
<p>...</p>
|
||||||
|
<a href="#">Read more …</a>
|
||||||
|
</div>
|
||||||
|
<a>Unsubscribe</a>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>You are not subscribed to any service.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>Available services</h2>
|
||||||
|
<div class="services">
|
||||||
|
{% for service in non_active_services %}
|
||||||
|
<div>
|
||||||
|
<div class="description">
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
|
<p>{{ service.description }}</p>
|
||||||
|
|
||||||
|
<a href="{% url "services:detail" service_slug=service.slug %}">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
<a href="{{ service.url }}" target="_blank">
|
||||||
|
Visit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="{% url "services:subscribe" service_slug=service.slug %}">Subscribe</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
1
src/services/tests.py
Normal file
1
src/services/tests.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Create your tests here.
|
114
src/services/views.py
Normal file
114
src/services/views.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
"""Views for the services app."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django_view_decorator import namespaced_decorator_factory
|
||||||
|
from membership.models import ServiceAccess
|
||||||
|
from utils.view_utils import render
|
||||||
|
|
||||||
|
from services.registry import ServiceInterface
|
||||||
|
from services.registry import ServiceRegistry
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
services_view = namespaced_decorator_factory(
|
||||||
|
namespace="services",
|
||||||
|
base_path="services",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@services_view(
|
||||||
|
paths="",
|
||||||
|
name="list",
|
||||||
|
login_required=True,
|
||||||
|
)
|
||||||
|
def services_overview(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""View all services."""
|
||||||
|
active_services = get_services(user=request.user)
|
||||||
|
|
||||||
|
active_service_classes = [service.__class__ for service in active_services]
|
||||||
|
|
||||||
|
services = [service for _, service in ServiceRegistry.get_items() if service not in active_service_classes]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"non_active_services": services,
|
||||||
|
"active_services": active_services,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="services/services_overview.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@services_view(
|
||||||
|
paths="<str:service_slug>/",
|
||||||
|
name="detail",
|
||||||
|
login_required=True,
|
||||||
|
)
|
||||||
|
def service_detail(request: HttpRequest, service_slug: str) -> HttpResponse:
|
||||||
|
"""View a service."""
|
||||||
|
service = ServiceRegistry.get(slug=service_slug)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"service": service,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="services/service_detail.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@services_view(
|
||||||
|
paths="<str:service_slug>/subscribe/",
|
||||||
|
name="subscribe",
|
||||||
|
login_required=True,
|
||||||
|
)
|
||||||
|
def service_subscribe(request: HttpRequest, service_slug: str) -> HttpResponse:
|
||||||
|
"""Subscribe to a service."""
|
||||||
|
service = ServiceRegistry.get(slug=service_slug)
|
||||||
|
|
||||||
|
form_class = service.get_form_class()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = form_class(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
data = form.cleaned_data
|
||||||
|
service_access = ServiceAccess(
|
||||||
|
user=request.user,
|
||||||
|
service=service.slug,
|
||||||
|
subscription_data=data,
|
||||||
|
)
|
||||||
|
service_access.save()
|
||||||
|
return redirect("services:list")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"service": service,
|
||||||
|
"base_path": "services:list",
|
||||||
|
"form": form_class(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="services/service_subscribe.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_services(*, user: User) -> list[ServiceInterface]:
|
||||||
|
"""Get services for the user."""
|
||||||
|
return [
|
||||||
|
access.service_implementation
|
||||||
|
for access in ServiceAccess.objects.filter(
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
]
|
Loading…
Reference in a new issue