Files
bugsink/projects/models.py
Klaas van Schelven 98e666e3ff Remove dashses from displayed DSN
Fixes #7

I could not find any documentation on what the "standard" is, but I know that
we'll pick it up just fine either way (because Django's UUID field does the
magic for us).

Given that it's impossible to setup your JS client with the dashes, they should
simply be removed.
2024-10-14 20:15:45 +02:00

133 lines
5.2 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/counted fields below
has_releases = models.BooleanField(editable=False, default=False)
digested_event_count = models.PositiveIntegerField(null=False, blank=False, default=0, editable=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)
# ingestion/digestion quota
quota_exceeded_until = models.DateTimeField(null=True, blank=True)
next_quota_check = models.PositiveIntegerField(null=False, default=0)
# retention
retention_max_event_count = models.PositiveIntegerField(default=10_000)
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.hex)
def get_latest_release(self):
from releases.models import ordered_releases
if not hasattr(self, "_latest_release"): # per-instance cache
self._latest_release = list(ordered_releases(project=self))[-1]
return self._latest_release
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