models.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import uuid
  2. from django.contrib.postgres.indexes import GinIndex
  3. from django.contrib.postgres.search import SearchVectorField
  4. from django.db import models
  5. from django.utils import timezone
  6. from psqlextra.models import PostgresPartitionedModel
  7. from psqlextra.types import PostgresPartitioningMethod
  8. from glitchtip.base_models import CreatedModel, SoftDeleteModel
  9. from sentry.constants import MAX_CULPRIT_LENGTH
  10. from .constants import EventStatus, IssueEventType, LogLevel
  11. from .utils import base32_encode
  12. class DeferedFieldManager(models.Manager):
  13. def __init__(self, defered_fields=[]):
  14. super().__init__()
  15. self.defered_fields = defered_fields
  16. def get_queryset(self, *args, **kwargs):
  17. return super().get_queryset(*args, **kwargs).defer(*self.defered_fields)
  18. class Issue(SoftDeleteModel):
  19. culprit = models.CharField(max_length=1024, blank=True, null=True)
  20. # has_seen = models.BooleanField(default=False)
  21. is_public = models.BooleanField(default=False)
  22. level = models.PositiveSmallIntegerField(
  23. choices=LogLevel.choices, default=LogLevel.ERROR
  24. )
  25. metadata = models.JSONField()
  26. project = models.ForeignKey(
  27. "projects.Project", on_delete=models.CASCADE, related_name="issues"
  28. )
  29. title = models.CharField(max_length=255)
  30. type = models.PositiveSmallIntegerField(
  31. choices=IssueEventType.choices, default=IssueEventType.DEFAULT
  32. )
  33. status = models.PositiveSmallIntegerField(
  34. choices=EventStatus.choices, default=EventStatus.UNRESOLVED
  35. )
  36. short_id = models.PositiveIntegerField(null=True)
  37. search_vector = SearchVectorField(editable=False, default="")
  38. count = models.PositiveIntegerField(default=1, editable=False)
  39. first_seen = models.DateTimeField(default=timezone.now, db_index=True)
  40. last_seen = models.DateTimeField(default=timezone.now, db_index=True)
  41. objects = DeferedFieldManager(["search_vector"])
  42. class Meta:
  43. base_manager_name = "objects"
  44. unique_together = (("project", "short_id"),)
  45. indexes = [GinIndex(fields=["search_vector"])]
  46. @property
  47. def short_id_display(self):
  48. """
  49. Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
  50. The intention is to be human readable identifiers that can reference an issue.
  51. """
  52. if self.short_id is not None:
  53. return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
  54. return ""
  55. class IssueHash(models.Model):
  56. issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="hashes")
  57. # Redundant project allows for unique constraint
  58. project = models.ForeignKey(
  59. "projects.Project", on_delete=models.CASCADE, related_name="+"
  60. )
  61. value = models.UUIDField(db_index=True)
  62. class Meta:
  63. constraints = [
  64. models.UniqueConstraint(
  65. fields=["project", "value"], name="issue hash project"
  66. )
  67. ]
  68. class Comment(models.Model):
  69. created = models.DateTimeField(auto_now_add=True)
  70. issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="comments")
  71. user = models.ForeignKey(
  72. "users.User", null=True, on_delete=models.SET_NULL, related_name="+"
  73. )
  74. text = models.TextField(blank=True, null=True)
  75. class Meta:
  76. ordering = ("-created",)
  77. class UserReport(CreatedModel):
  78. project = models.ForeignKey(
  79. "projects.Project", on_delete=models.CASCADE, related_name="+"
  80. )
  81. issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
  82. event_id = models.UUIDField()
  83. name = models.CharField(max_length=128)
  84. email = models.EmailField()
  85. comments = models.TextField()
  86. class Meta:
  87. unique_together = (("project", "event_id"),)
  88. class IssueEvent(PostgresPartitionedModel, models.Model):
  89. id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  90. issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
  91. type = models.PositiveSmallIntegerField(default=0, choices=IssueEventType.choices)
  92. timestamp = models.DateTimeField(help_text="Time at which event happened")
  93. received = models.DateTimeField(help_text="Time at which GlitchTip accepted event")
  94. title = models.CharField(max_length=255)
  95. transaction = models.CharField(max_length=MAX_CULPRIT_LENGTH)
  96. level = models.PositiveSmallIntegerField(
  97. choices=LogLevel.choices, default=LogLevel.ERROR
  98. )
  99. data = models.JSONField()
  100. # This could be HStore, but jsonb is just as good and removes need for
  101. # 'django.contrib.postgres' which makes several unnecessary SQL calls
  102. tags = models.JSONField()
  103. class PartitioningMeta:
  104. method = PostgresPartitioningMethod.RANGE
  105. key = ["received"]
  106. def __str__(self):
  107. return self.eventID
  108. @property
  109. def eventID(self):
  110. return self.id.hex
  111. @property
  112. def message(self):
  113. """Often the title and message are the same. If message isn't stored, assume it's the title"""
  114. return self.data.get("message", self.title)
  115. @property
  116. def metadata(self):
  117. """Return metadata if exists, else return just the title as metadata"""
  118. return self.data.get("metadata", {"title": self.title})
  119. @property
  120. def platform(self):
  121. return self.data.get("platform")