Source code for swh.coarnotify.server.models
# Copyright (C) 2025 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
"""Models."""
from __future__ import annotations
from typing import cast
import uuid
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models
from django.urls import reverse
[docs]
class Organization(models.Model):
"""An organization."""
name = models.CharField(max_length=150, blank=True)
url = models.URLField(null=False, unique=True)
"""Organization URL (for discovery)."""
inbox = models.URLField(null=False, unique=True)
"""LDN inbox URL."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self) -> str:
return f"{self.name}"
[docs]
class ActorManager(BaseUserManager):
[docs]
def create_user(
self, email: str, password: str | None = None, **extra_fields
) -> "Actor":
if not email:
raise ValueError("Email is required")
instance = self.model(email=self.normalize_email(email), **extra_fields)
user = cast(Actor, instance)
user.set_password(password)
user.save()
return user
[docs]
def create_superuser(self, email: str, password: str, **extra_fields) -> "Actor":
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return self.create_user(email, password, **extra_fields)
[docs]
class Actor(AbstractUser):
"""A member of an organization."""
email = models.EmailField(unique=True)
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
name = models.CharField(max_length=150, blank=True)
# mypy is not happy with this but it follows the recommended way of overriding
# django's user model
username = None # type: ignore[assignment]
first_name = None # type: ignore[assignment]
last_name = None # type: ignore[assignment]
objects = ActorManager() # type: ignore[misc,assignment]
USERNAME_FIELD = "email"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = ["organization"]
def __str__(self) -> str:
return f"{self.name} <{self.email}>"
[docs]
class Statuses(models.TextChoices):
PENDING = "pending", "Pending"
REJECTED = "rejected", "Rejected"
ACCEPTED = "accepted", "Accepted"
PROCESSED = "processed", "Processed"
UNPROCESSABLE = "unprocessable", "Unprocessable"
[docs]
class Notification(models.Model):
"""An abstract model for COAR Notification."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
status = models.CharField(max_length=20, choices=Statuses, default=Statuses.PENDING)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
payload = models.JSONField()
error_message = models.TextField()
def __str__(self) -> str:
return str(self.id)
[docs]
class InboundNotification(Notification):
"""Our inbox"""
raw_payload = models.JSONField()
in_reply_to = models.ForeignKey(
"OutboundNotification",
null=True,
on_delete=models.SET_NULL,
related_name="replied_by",
)
sender = models.ForeignKey(Organization, on_delete=models.SET_NULL, null=True)
[docs]
def get_absolute_url(self) -> str:
return reverse("read", kwargs={"pk": self.pk})
[docs]
class OutboundNotification(Notification):
"""Our outbox."""
in_reply_to = models.ForeignKey(
"InboundNotification",
null=True,
on_delete=models.SET_NULL,
related_name="replied_by",
)