models.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. from allauth.socialaccount.models import SocialApp
  2. from django.conf import settings
  3. from django.core.validators import MaxValueValidator
  4. from django.db import models
  5. from django.db.models import Exists, F, OuterRef, Q
  6. from django.db.models.functions import Coalesce
  7. from django.utils.text import slugify
  8. from django.utils.translation import gettext_lazy as _
  9. from organizations.abstract import SharedBaseModel
  10. from organizations.base import (
  11. OrganizationBase,
  12. OrganizationInvitationBase,
  13. OrganizationOwnerBase,
  14. OrganizationUserBase,
  15. )
  16. from organizations.managers import OrgManager
  17. from organizations.signals import owner_changed, user_added
  18. from sql_util.utils import SubqueryCount, SubquerySum
  19. from apps.observability.metrics import clear_metrics_cache
  20. from .constants import OrganizationUserRole
  21. from .fields import OrganizationSlugField
  22. class OrganizationManager(OrgManager):
  23. def with_event_counts(self, current_period=True):
  24. queryset = self
  25. subscription_filter = Q()
  26. event_subscription_filter = Q()
  27. checks_subscription_filter = Q()
  28. if current_period and settings.BILLING_ENABLED:
  29. from djstripe.models import Subscription
  30. subscription_filter = Q(
  31. created__gte=OuterRef(
  32. "djstripe_customers__subscriptions__current_period_start"
  33. ),
  34. created__lt=OuterRef(
  35. "djstripe_customers__subscriptions__current_period_end"
  36. ),
  37. )
  38. event_subscription_filter = Q(
  39. date__gte=OuterRef(
  40. "djstripe_customers__subscriptions__current_period_start"
  41. ),
  42. date__lt=OuterRef(
  43. "djstripe_customers__subscriptions__current_period_end"
  44. ),
  45. )
  46. checks_subscription_filter = Q(
  47. start_check__gte=OuterRef(
  48. "djstripe_customers__subscriptions__current_period_start"
  49. ),
  50. start_check__lt=OuterRef(
  51. "djstripe_customers__subscriptions__current_period_end"
  52. ),
  53. )
  54. # If the org has an active sub, filter by it. If not, do not filter.
  55. # This forces the exclusion of inactive subscriptions in subquery counts
  56. queryset = queryset.annotate(
  57. has_active_subscription=Exists(
  58. Subscription.objects.filter(
  59. customer=OuterRef("djstripe_customers"),
  60. status="active",
  61. )
  62. )
  63. ).filter(
  64. Q(
  65. has_active_subscription=True,
  66. djstripe_customers__subscriptions__status="active",
  67. )
  68. | Q(has_active_subscription=False)
  69. )
  70. queryset = queryset.annotate(
  71. issue_event_count=Coalesce(
  72. SubquerySum(
  73. "projects__issueeventprojecthourlystatistic__count",
  74. filter=event_subscription_filter,
  75. ),
  76. 0,
  77. ),
  78. transaction_count=Coalesce(
  79. SubquerySum(
  80. "projects__transactioneventprojecthourlystatistic__count",
  81. filter=event_subscription_filter,
  82. ),
  83. 0,
  84. ),
  85. uptime_check_event_count=SubqueryCount(
  86. "monitor__checks", filter=checks_subscription_filter
  87. ),
  88. file_size=(
  89. Coalesce(
  90. SubquerySum(
  91. "debugsymbolbundle__file__blob__size",
  92. filter=subscription_filter,
  93. ),
  94. 0,
  95. )
  96. + Coalesce(
  97. SubquerySum(
  98. "projects__debuginformationfile__file__blob__size",
  99. filter=subscription_filter,
  100. ),
  101. 0,
  102. )
  103. )
  104. / 1000000,
  105. total_event_count=F("issue_event_count")
  106. + F("transaction_count")
  107. + F("uptime_check_event_count")
  108. + F("file_size"),
  109. )
  110. return queryset.distinct("pk")
  111. class Organization(SharedBaseModel, OrganizationBase):
  112. slug = OrganizationSlugField(
  113. max_length=200,
  114. blank=False,
  115. editable=True,
  116. populate_from="name",
  117. unique=True,
  118. help_text=_("The name in all lowercase, suitable for URL identification"),
  119. )
  120. is_accepting_events = models.BooleanField(
  121. default=True, help_text="Used for throttling at org level"
  122. )
  123. event_throttle_rate = models.PositiveSmallIntegerField(
  124. default=0,
  125. validators=[MaxValueValidator(100)],
  126. help_text="Probability (in percent) on how many events are throttled. Used for throttling at project level",
  127. )
  128. open_membership = models.BooleanField(
  129. default=True, help_text="Allow any organization member to join any team"
  130. )
  131. scrub_ip_addresses = models.BooleanField(
  132. default=True,
  133. help_text="Default for whether projects should script IP Addresses",
  134. )
  135. stripe_customer_id = models.CharField(max_length=28, blank=True)
  136. stripe_primary_subscription = models.ForeignKey(
  137. "stripe.StripeSubscription",
  138. on_delete=models.SET_NULL,
  139. blank=True,
  140. null=True,
  141. related_name="+",
  142. )
  143. objects = OrganizationManager()
  144. def save(self, *args, **kwargs):
  145. new = False
  146. if not self.pk:
  147. new = True
  148. super().save(*args, **kwargs)
  149. if new:
  150. clear_metrics_cache()
  151. def delete(self, *args, **kwargs):
  152. super().delete(*args, **kwargs)
  153. clear_metrics_cache()
  154. def slugify_function(self, content):
  155. reserved_words = [
  156. "login",
  157. "register",
  158. "app",
  159. "profile",
  160. "organizations",
  161. "settings",
  162. "issues",
  163. "performance",
  164. "_health",
  165. "rest-auth",
  166. "api",
  167. "accept",
  168. "stripe",
  169. "admin",
  170. "status_page",
  171. "__debug__",
  172. ]
  173. slug = slugify(content)
  174. if slug in reserved_words:
  175. return slug + "-1"
  176. return slug
  177. def add_user(self, user, role=OrganizationUserRole.MEMBER):
  178. """
  179. Adds a new user and if the first user makes the user an admin and
  180. the owner.
  181. """
  182. users_count = self.users.all().count()
  183. if users_count == 0:
  184. role = OrganizationUserRole.OWNER
  185. org_user = self._org_user_model.objects.create(
  186. user=user, organization=self, role=role
  187. )
  188. if users_count == 0:
  189. self._org_owner_model.objects.create(
  190. organization=self, organization_user=org_user
  191. )
  192. # User added signal
  193. user_added.send(sender=self, user=user)
  194. return org_user
  195. @property
  196. def owners(self):
  197. return self.users.filter(
  198. organizations_ext_organizationuser__role=OrganizationUserRole.OWNER
  199. )
  200. @property
  201. def email(self):
  202. """Used to identify billing contact for stripe."""
  203. billing_contact = self.owner.organization_user.user
  204. return billing_contact.email
  205. def get_user_scopes(self, user):
  206. org_user = self.organization_users.get(user=user)
  207. return org_user.get_scopes()
  208. def change_owner(self, new_owner):
  209. """
  210. Changes ownership of an organization.
  211. """
  212. old_owner = self.owner.organization_user
  213. self.owner.organization_user = new_owner
  214. self.owner.save()
  215. owner_changed.send(sender=self, old=old_owner, new=new_owner)
  216. def is_owner(self, user):
  217. """
  218. Returns True is user is the organization's owner, otherwise false
  219. """
  220. return self.owner.organization_user.user == user
  221. class OrganizationUser(SharedBaseModel, OrganizationUserBase):
  222. user = models.ForeignKey(
  223. "users.User",
  224. blank=True,
  225. null=True,
  226. on_delete=models.CASCADE,
  227. related_name="organizations_ext_organizationuser",
  228. )
  229. role = models.PositiveSmallIntegerField(choices=OrganizationUserRole.choices)
  230. email = models.EmailField(
  231. blank=True, null=True, help_text="Email for pending invite"
  232. )
  233. class Meta(OrganizationOwnerBase.Meta):
  234. unique_together = (("user", "organization"), ("email", "organization"))
  235. def __str__(self, *args, **kwargs):
  236. if self.user:
  237. return super().__str__(*args, **kwargs)
  238. return self.email
  239. def get_email(self):
  240. if self.user:
  241. return self.user.email
  242. return self.email
  243. def get_role(self):
  244. return self.get_role_display().lower()
  245. def get_scopes(self):
  246. role = OrganizationUserRole.get_role(self.role)
  247. return role["scopes"]
  248. @property
  249. def pending(self):
  250. return self.user_id is None
  251. @property
  252. def is_active(self):
  253. """Non pending means active"""
  254. return not self.pending
  255. class OrganizationOwner(OrganizationOwnerBase):
  256. """Only usage is for billing contact currently"""
  257. class OrganizationInvitation(OrganizationInvitationBase):
  258. """Required to exist for django-organizations"""
  259. class OrganizationSocialApp(models.Model):
  260. """
  261. Associate organization with social app, for authentication purposes.
  262. Example: If Foo org has social app FooGoogle, then any user logging in via FooGoogle
  263. OAuth must be automatically assigned to the Foo org.
  264. """
  265. organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
  266. social_app = models.OneToOneField(SocialApp, on_delete=models.CASCADE)