WIP: Services #25
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-07-15 07:30
|
||||||
|
|
||||||
|
import django_registries.registry
|
||||||
|
import services.registry
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('membership', '0006_serviceaccess_serviceaccess_unique_user_service'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='serviceaccess',
|
||||||
|
name='subscription_data',
|
||||||
|
field=models.JSONField(blank=True, null=True, verbose_name='subscription data'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='serviceaccess',
|
||||||
|
name='service',
|
||||||
|
field=django_registries.registry.ChoicesField(choices=[('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,5 @@
|
||||||
"""Models for the membership app."""
|
"""Models for the membership app."""
|
||||||
|
|
||||||
from typing import ClassVar
|
|
||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -10,7 +9,6 @@ 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 services.registry import ServiceRegistry
|
||||||
from utils.mixins import CreatedModifiedAbstract
|
from utils.mixins import CreatedModifiedAbstract
|
||||||
|
|
||||||
|
@ -58,14 +56,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')}"
|
||||||
|
@ -144,19 +142,27 @@ class MembershipType(CreatedModifiedAbstract):
|
||||||
|
|
||||||
|
|
||||||
class ServiceAccess(CreatedModifiedAbstract):
|
class ServiceAccess(CreatedModifiedAbstract):
|
||||||
|
"""Access to a service for a user."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("service access")
|
verbose_name = _("service access")
|
||||||
verbose_name_plural = _("service accesses")
|
verbose_name_plural = _("service accesses")
|
||||||
constraints = [
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=["user", "service"],
|
fields=["user", "service"],
|
||||||
name="unique_user_service",
|
name="unique_user_service",
|
||||||
),
|
),
|
||||||
]
|
)
|
||||||
|
|
||||||
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||||
|
|||||||
|
|
||||||
service = ServiceRegistry.choices_field(verbose_name=_("service"))
|
service = ServiceRegistry.choices_field(verbose_name=_("service"))
|
||||||
|
|
||||||
def __str__(self):
|
subscription_data = models.JSONField(
|
||||||
|
verbose_name=_("subscription data"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
return f"{self.user} - {self.service}"
|
return f"{self.user} - {self.service}"
|
||||||
benjaoming
commented
Just wanna say that I've seen this WRT the Membership and MembershipType definitions. I think this will work fine in combination. I have some ideas etc.. but fundamentally, this doesn't need to change 👍 Just wanna say that I've seen this WRT the Membership and MembershipType definitions. I think this will work fine in combination. I have some ideas etc.. but fundamentally, this doesn't need to change 👍
benjaoming
commented
There will be a new model Product and Order that are relevant for charging services, but we can probably figure out how they are associated with services... I like the idea of matching a service with some well-known string. There will be a new model Product and Order that are relevant for charging services, but we can probably figure out how they are associated with services... I like the idea of matching a service with some well-known string.
|
|||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
|
"""Registry for services."""
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django_registries.registry import Interface
|
from django_registries.registry import Interface
|
||||||
from django_registries.registry import Registry
|
from django_registries.registry import Registry
|
||||||
|
|
||||||
|
|
||||||
class ServiceRegistry(Registry):
|
class ServiceRegistry(Registry):
|
||||||
"""Registry for services"""
|
"""Registry for services."""
|
||||||
|
|
||||||
implementations_module = "services"
|
implementations_module = "services"
|
||||||
|
|
||||||
|
|
||||||
class ServiceInterface(Interface):
|
class ServiceInterface(Interface):
|
||||||
"""Interface for services"""
|
"""Interface for services."""
|
||||||
|
|
||||||
registry = ServiceRegistry
|
registry = ServiceRegistry
|
||||||
|
|
||||||
|
@ -25,16 +27,12 @@ class ServiceInterface(Interface):
|
||||||
# this could be used to generate a form for the service, and also to validate
|
# 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
|
# the data saved in a JSONField on the ServiceAccess model
|
||||||
|
|
||||||
subscribe_fields: list[tuple[str, forms.Field]] = []
|
subscribe_fields: tuple[tuple[str, forms.Field]] = []
|
||||||
|
|
||||||
def get_form(self) -> type:
|
def get_form_class(self) -> type:
|
||||||
"""Get the form for the service"""
|
"""Get the form class for the service."""
|
||||||
print(self.subscribe_fields)
|
|
||||||
return type(
|
return type(
|
||||||
"ServiceForm",
|
"ServiceForm",
|
||||||
(forms.Form,),
|
(forms.Form,),
|
||||||
{
|
dict(self.subscribe_fields),
|
||||||
field_name: field_type
|
)
|
||||||
for field_name, field_type in self.subscribe_fields
|
|
||||||
},
|
|
||||||
)()
|
|
||||||
|
|
|
@ -1,34 +1,46 @@
|
||||||
|
"""Service classes for data.coop."""
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .registry import ServiceInterface
|
from .registry import ServiceInterface
|
||||||
|
|
||||||
|
|
||||||
class MailService(ServiceInterface):
|
class MailService(ServiceInterface):
|
||||||
|
"""Mail service."""
|
||||||
|
|
||||||
slug = "mail"
|
slug = "mail"
|
||||||
name = "Mail"
|
name = "Mail"
|
||||||
url = "https://mail.data.coop"
|
url = "https://mail.data.coop"
|
||||||
description = "Mail service for data.coop"
|
description = "Mail service for data.coop"
|
||||||
|
|
||||||
|
subscribe_fields = (("username", forms.CharField()),)
|
||||||
|
|
||||||
|
|
||||||
class MatrixService(ServiceInterface):
|
class MatrixService(ServiceInterface):
|
||||||
|
"""Matrix service."""
|
||||||
|
|
||||||
slug = "matrix"
|
slug = "matrix"
|
||||||
name = "Matrix"
|
name = "Matrix"
|
||||||
url = "https://matrix.data.coop"
|
url = "https://matrix.data.coop"
|
||||||
description = "Matrix service for data.coop"
|
description = "Matrix service for data.coop"
|
||||||
|
|
||||||
subscribe_fields = [
|
subscribe_fields = (("username", forms.CharField()),)
|
||||||
("username", forms.CharField()),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class MastodonService(ServiceInterface):
|
class MastodonService(ServiceInterface):
|
||||||
|
"""Mastodon service."""
|
||||||
|
|
||||||
slug = "mastodon"
|
slug = "mastodon"
|
||||||
name = "Mastodon"
|
name = "Mastodon"
|
||||||
url = "https://social.data.coop"
|
url = "https://social.data.coop"
|
||||||
description = "Mastodon service for data.coop"
|
description = "Mastodon service for data.coop"
|
||||||
|
|
||||||
|
subscribe_fields = (("username", forms.CharField()),)
|
||||||
|
|
||||||
|
|
||||||
class NextcloudService(ServiceInterface):
|
class NextcloudService(ServiceInterface):
|
||||||
|
"""Nextcloud service."""
|
||||||
|
|
||||||
slug = "nextcloud"
|
slug = "nextcloud"
|
||||||
name = "Nextcloud"
|
name = "Nextcloud"
|
||||||
url = "https://cloud.data.coop"
|
url = "https://cloud.data.coop"
|
||||||
|
@ -36,6 +48,8 @@ class NextcloudService(ServiceInterface):
|
||||||
|
|
||||||
|
|
||||||
class HedgeDocService(ServiceInterface):
|
class HedgeDocService(ServiceInterface):
|
||||||
|
"""HedgeDoc service."""
|
||||||
|
|
||||||
slug = "hedgedoc"
|
slug = "hedgedoc"
|
||||||
name = "HedgeDoc"
|
name = "HedgeDoc"
|
||||||
url = "https://pad.data.coop"
|
url = "https://pad.data.coop"
|
||||||
|
@ -44,6 +58,8 @@ class HedgeDocService(ServiceInterface):
|
||||||
|
|
||||||
|
|
||||||
class RalllyService(ServiceInterface):
|
class RalllyService(ServiceInterface):
|
||||||
|
"""Rallly service."""
|
||||||
|
|
||||||
slug = "rallly"
|
slug = "rallly"
|
||||||
name = "Rallly"
|
name = "Rallly"
|
||||||
url = "https://when.data.coop"
|
url = "https://when.data.coop"
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
<div class="content-view">
|
<div class="content-view">
|
||||||
<h2>Subscribe to {{ service.name }}</h2>
|
<h2>Subscribe to {{ service.name }}</h2>
|
||||||
|
|
||||||
<form>
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
|
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
# Create your views here.
|
"""Views for the services app."""
|
||||||
from django_view_decorator import namespaced_decorator_factory
|
|
||||||
|
|
||||||
|
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 membership.models import ServiceAccess
|
||||||
from services.registry import ServiceRegistry
|
|
||||||
from utils.view_utils import render
|
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(
|
services_view = namespaced_decorator_factory(
|
||||||
namespace="services",
|
namespace="services",
|
||||||
|
@ -17,21 +28,13 @@ services_view = namespaced_decorator_factory(
|
||||||
name="list",
|
name="list",
|
||||||
login_required=True,
|
login_required=True,
|
||||||
)
|
)
|
||||||
def services_overview(request):
|
def services_overview(request: HttpRequest) -> HttpResponse:
|
||||||
active_services = [
|
"""View all services."""
|
||||||
access.service_implementation
|
active_services = get_services(user=request.user)
|
||||||
for access in ServiceAccess.objects.filter(
|
|
||||||
user=request.user,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
active_service_classes = [service.__class__ for service in active_services]
|
active_service_classes = [service.__class__ for service in active_services]
|
||||||
|
|
||||||
services = [
|
services = [service for _, service in ServiceRegistry.get_items() if service not in active_service_classes]
|
||||||
service
|
|
||||||
for _, service in ServiceRegistry.get_items()
|
|
||||||
if service not in active_service_classes
|
|
||||||
]
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"non_active_services": services,
|
"non_active_services": services,
|
||||||
|
@ -50,7 +53,8 @@ def services_overview(request):
|
||||||
name="detail",
|
name="detail",
|
||||||
login_required=True,
|
login_required=True,
|
||||||
)
|
)
|
||||||
def service_detail(request, service_slug):
|
def service_detail(request: HttpRequest, service_slug: str) -> HttpResponse:
|
||||||
|
"""View a service."""
|
||||||
service = ServiceRegistry.get(slug=service_slug)
|
service = ServiceRegistry.get(slug=service_slug)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
@ -69,14 +73,28 @@ def service_detail(request, service_slug):
|
||||||
name="subscribe",
|
name="subscribe",
|
||||||
login_required=True,
|
login_required=True,
|
||||||
)
|
)
|
||||||
def service_subscribe(request, service_slug):
|
def service_subscribe(request: HttpRequest, service_slug: str) -> HttpResponse:
|
||||||
|
"""Subscribe to a service."""
|
||||||
service = ServiceRegistry.get(slug=service_slug)
|
service = ServiceRegistry.get(slug=service_slug)
|
||||||
|
|
||||||
# TODO: add a form to subscribe to the service
|
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 = {
|
context = {
|
||||||
"service": service,
|
"service": service,
|
||||||
"base_path": "services:list",
|
"base_path": "services:list",
|
||||||
"form": service.get_form(),
|
"form": form_class(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
|
@ -84,3 +102,13 @@ def service_subscribe(request, service_slug):
|
||||||
template_name="services/service_subscribe.html",
|
template_name="services/service_subscribe.html",
|
||||||
context=context,
|
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
This is correct! 💯
Notable because there is also a Membership model.. however, memberships are renewed every year, and a user should be able to use the same services across different memberships.
How we handle "eligibility to a service" can happen later..