Files
bugsink/projects/models.py
2024-06-21 09:28:04 +02:00

137 lines
5.3 KiB
Python

import uuid
from django.db import models
from django.conf import settings
from django.utils.text import slugify
from bugsink.app_settings import get_settings
from compat.dsn import build_dsn
from teams.models import TeamMembership, TeamRole
class ProjectRole(models.IntegerChoices):
MEMBER = 0
ADMIN = 1
class ProjectVisibility(models.IntegerChoices):
# PUBLIC = 0 # anyone can see the project and its members; not sure if I want this or always require click-in
JOINABLE = 1 # anyone can join
DISCOVERABLE = 10 # the project's existance is visible, you can request to join(?), but this needs to be approved
TEAM_MEMBERS = 99 # the project is only visible to team-members (and for some(?) things they need to click "join")
class Project(models.Model):
# id is implied which makes it an Integer; we would prefer a uuid but the sentry clients have int baked into the DSN
# parser (we could also introduce a special field for that purpose but that's ugly too)
team = models.ForeignKey("teams.Team", blank=False, null=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
slug = models.SlugField(max_length=50, blank=False, null=False, unique=True)
# sentry_key mirrors the "public" part of the sentry DSN. As of late 2023 Sentry's docs say the this about DSNs:
#
# > DSNs are safe to keep public because they only allow submission of new events and related event data; they do
# > not allow read access to any information.
#
# The "because" in that sentence is dubious at least; however, I get why they say it, because they want to do JS and
# native apps too, and there's really no way to do those without exposing (some) endpoint. Anyway, I don't think the
# "public" key is public, and if you can help it it's always better to keep it private.
sentry_key = models.UUIDField(editable=False, default=uuid.uuid4)
users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, through="ProjectMembership")
# We don't implement private_key because as of late 2023 the Sentry documentation says the following:
# > The secret part of the DSN is optional and effectively deprecated. While clients will still honor it, if
# > supplied, future versions of Sentry will entirely ignore it.
# private_key = ...
# denormalized/cached fields below
has_releases = models.BooleanField(editable=False, default=False)
# alerting conditions
alert_on_new_issue = models.BooleanField(default=True)
alert_on_regression = models.BooleanField(default=True)
alert_on_unmute = models.BooleanField(default=True)
# visibility
visibility = models.IntegerField(choices=ProjectVisibility.choices, default=ProjectVisibility.TEAM_MEMBERS)
# retention quota
retention_event_count_quota = models.PositiveIntegerField(default=10_000)
# bookkeeping of the eviction algorithm
retention_last_eviction = models.DateTimeField(null=True, blank=True)
retention_max_total_irrelevance = models.PositiveIntegerField(null=True, blank=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return f"/issues/{ self.id }/"
@property
def dsn(self):
return build_dsn(get_settings().BASE_URL, self.id, self.sentry_key)
"""
# TODO is this even more efficient?
indexes = [
models.Index(fields=["id", "sentry_key"]),
]
"""
def get_latest_release(self):
# TODO perfomance considerations... this can be denormalized/cached at the project level
from releases.models import ordered_releases
return list(ordered_releases(project=self))[-1]
def save(self, *args, **kwargs):
if self.slug in [None, ""]:
# we don't want to have empty slugs, so we'll generate a unique one
base_slug = slugify(self.name)
similar_slugs = Project.objects.filter(slug__startswith=base_slug).values_list("slug", flat=True)
self.slug = base_slug
i = 0
while self.slug in similar_slugs:
self.slug = f"{base_slug}-{i}"
i += 1
super().save(*args, **kwargs)
def is_joinable(self, user=None):
if user is not None:
# take the user's team membership into account
try:
tm = TeamMembership.objects.get(team=self.team, user=user)
if tm.role == TeamRole.ADMIN:
return True
except TeamMembership.DoesNotExist:
pass
return self.visibility <= ProjectVisibility.JOINABLE
def is_discoverable(self):
return self.visibility <= ProjectVisibility.DISCOVERABLE
class ProjectMembership(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
send_email_alerts = models.BooleanField(default=None, null=True)
role = models.IntegerField(choices=ProjectRole.choices, default=ProjectRole.MEMBER)
accepted = models.BooleanField(default=False)
def __str__(self):
return f"{self.user} project membership of {self.project}"
class Meta:
unique_together = ("project", "user")
def is_admin(self):
return self.role == ProjectRole.ADMIN