mirror of
https://github.com/bugsink/bugsink.git
synced 2025-12-31 10:20:08 -06:00
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.
133 lines
5.2 KiB
Python
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
|