def test_extract_low(self): """Extract low 3 bits""" low_bits = BitField(0, 3) self.assertEqual(low_bits.extract(0b10101010101), 0b101)
class Project(Model): """ Projects are permission based namespaces which generally are the top level entry point for all data. """ __core__ = True slug = models.SlugField(null=True) name = models.CharField(max_length=200) forced_color = models.CharField(max_length=6, null=True, blank=True) organization = FlexibleForeignKey('sentry.Organization') team = FlexibleForeignKey('sentry.Team') teams = models.ManyToManyField('sentry.Team', related_name='teams', through=ProjectTeam) public = models.BooleanField(default=False) date_added = models.DateTimeField(default=timezone.now) status = BoundedPositiveIntegerField( default=0, choices=( (ObjectStatus.VISIBLE, _('Active')), (ObjectStatus.PENDING_DELETION, _('Pending Deletion')), (ObjectStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')), ), db_index=True) # projects that were created before this field was present # will have their first_event field set to date_added first_event = models.DateTimeField(null=True) flags = BitField(flags=(('has_releases', 'This Project has sent release data'), ), default=0, null=True) objects = ProjectManager(cache_fields=[ 'pk', 'slug', ]) platform = models.CharField(max_length=64, null=True) class Meta: app_label = 'sentry' db_table = 'sentry_project' unique_together = (('team', 'slug'), ('organization', 'slug')) __repr__ = sane_repr('team_id', 'name', 'slug') def __unicode__(self): return u'%s (%s)' % (self.name, self.slug) def next_short_id(self): from sentry.models import Counter return Counter.increment(self) def save(self, *args, **kwargs): if not self.slug: lock = locks.get('slug:project', duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance(self, self.name, organization=self.organization) super(Project, self).save(*args, **kwargs) else: super(Project, self).save(*args, **kwargs) def get_absolute_url(self): return absolute_uri('/{}/{}/'.format(self.organization.slug, self.slug)) def is_internal_project(self): for value in (settings.SENTRY_FRONTEND_PROJECT, settings.SENTRY_PROJECT): if six.text_type(self.id) == six.text_type(value) or six.text_type( self.slug) == six.text_type(value): return True return False # TODO: Make these a mixin def update_option(self, *args, **kwargs): from sentry.models import ProjectOption return ProjectOption.objects.set_value(self, *args, **kwargs) def get_option(self, *args, **kwargs): from sentry.models import ProjectOption return ProjectOption.objects.get_value(self, *args, **kwargs) def delete_option(self, *args, **kwargs): from sentry.models import ProjectOption return ProjectOption.objects.unset_value(self, *args, **kwargs) @property def callsign(self): return self.slug.upper() @property def color(self): if self.forced_color is not None: return '#%s' % self.forced_color return get_hashed_color(self.callsign or self.slug) @property def member_set(self): from sentry.models import OrganizationMember return self.organization.member_set.filter( id__in=OrganizationMember.objects.filter( organizationmemberteam__is_active=True, organizationmemberteam__team=self.team, ).values('id'), user__is_active=True, ).distinct() def has_access(self, user, access=None): from sentry.models import AuthIdentity, OrganizationMember warnings.warn('Project.has_access is deprecated.', DeprecationWarning) queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) try: member = queryset.get() except OrganizationMember.DoesNotExist: return False try: auth_identity = AuthIdentity.objects.get( auth_provider__organization=self.organization_id, user=member.user_id, ) except AuthIdentity.DoesNotExist: return True return auth_identity.is_valid(member) def get_audit_log_data(self): return { 'id': self.id, 'slug': self.slug, 'name': self.name, 'status': self.status, 'public': self.public, } def get_full_name(self): if self.team.name not in self.name: return '%s %s' % (self.team.name, self.name) return self.name def get_notification_recipients(self, user_option): from sentry.models import UserOption alert_settings = dict((o.user_id, int(o.value)) for o in UserOption.objects.filter( project=self, key=user_option, )) disabled = set(u for u, v in six.iteritems(alert_settings) if v == 0) member_set = set( self.member_set.exclude(user__in=disabled, ).values_list( 'user', flat=True)) # determine members default settings members_to_check = set(u for u in member_set if u not in alert_settings) if members_to_check: disabled = set((uo.user_id for uo in UserOption.objects.filter( key='subscribe_by_default', user__in=members_to_check, ) if uo.value == '0')) member_set = [x for x in member_set if x not in disabled] return member_set def get_mail_alert_subscribers(self): user_ids = self.get_notification_recipients('mail:alert') if not user_ids: return [] from sentry.models import User return list(User.objects.filter(id__in=user_ids)) def is_user_subscribed_to_mail_alerts(self, user): from sentry.models import UserOption is_enabled = UserOption.objects.get_value(user, 'mail:alert', project=self) if is_enabled is None: is_enabled = UserOption.objects.get_value(user, 'subscribe_by_default', '1') == '1' else: is_enabled = bool(is_enabled) return is_enabled def transfer_to(self, team): from sentry.models import ProjectTeam, ReleaseProject organization = team.organization from_team_id = self.team_id # We only need to delete ReleaseProjects when moving to a different # Organization. Releases are bound to Organization, so it's not realistic # to keep this link unless we say, copied all Releases as well. if self.organization_id != organization.id: ReleaseProject.objects.filter(project_id=self.id, ).delete() self.organization = organization self.team = team try: with transaction.atomic(): self.update( organization=organization, team=team, ) except IntegrityError: slugify_instance(self, self.name, organization=organization) self.update( slug=self.slug, organization=organization, team=team, ) ProjectTeam.objects.filter(project=self, team_id=from_team_id).update(team=team) def add_team(self, team): try: with transaction.atomic(): ProjectTeam.objects.create(project=self, team=team) except IntegrityError: return False else: return True def get_security_token(self): lock = locks.get(self.get_lock_key(), duration=5) with TimedRetryPolicy(10)(lock.acquire): security_token = self.get_option('sentry:token', None) if security_token is None: security_token = uuid1().hex self.update_option('sentry:token', security_token) return security_token def get_lock_key(self): return 'project_token:%s' % self.id
class Project(Model): """ Projects are permission based namespaces which generally are the top level entry point for all data. """ __core__ = True slug = models.SlugField(null=True) name = models.CharField(max_length=200) forced_color = models.CharField(max_length=6, null=True, blank=True) organization = FlexibleForeignKey('sentry.Organization') teams = models.ManyToManyField( 'sentry.Team', related_name='teams', through=ProjectTeam ) public = models.BooleanField(default=False) date_added = models.DateTimeField(default=timezone.now) status = BoundedPositiveIntegerField( default=0, choices=( (ObjectStatus.VISIBLE, _('Active')), (ObjectStatus.PENDING_DELETION, _('Pending Deletion')), (ObjectStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')), ), db_index=True ) # projects that were created before this field was present # will have their first_event field set to date_added first_event = models.DateTimeField(null=True) flags = BitField( flags=(('has_releases', 'This Project has sent release data'), ), default=0, null=True ) objects = ProjectManager(cache_fields=[ 'pk', 'slug', ]) platform = models.CharField(max_length=64, null=True) class Meta: app_label = 'sentry' db_table = 'sentry_project' unique_together = (('organization', 'slug'),) __repr__ = sane_repr('team_id', 'name', 'slug') def __unicode__(self): return u'%s (%s)' % (self.name, self.slug) def next_short_id(self): from sentry.models import Counter return Counter.increment(self) def save(self, *args, **kwargs): if not self.slug: lock = locks.get('slug:project', duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance( self, self.name, organization=self.organization, reserved=RESERVED_PROJECT_SLUGS) super(Project, self).save(*args, **kwargs) else: super(Project, self).save(*args, **kwargs) def get_absolute_url(self): return absolute_uri('/{}/{}/'.format(self.organization.slug, self.slug)) def is_internal_project(self): for value in (settings.SENTRY_FRONTEND_PROJECT, settings.SENTRY_PROJECT): if six.text_type(self.id) == six.text_type(value) or six.text_type( self.slug ) == six.text_type(value): return True return False # TODO: Make these a mixin def update_option(self, *args, **kwargs): from sentry.models import ProjectOption return ProjectOption.objects.set_value(self, *args, **kwargs) def get_option(self, *args, **kwargs): from sentry.models import ProjectOption return ProjectOption.objects.get_value(self, *args, **kwargs) def delete_option(self, *args, **kwargs): from sentry.models import ProjectOption return ProjectOption.objects.unset_value(self, *args, **kwargs) @property def callsign(self): warnings.warn( 'Project.callsign is deprecated. Use Group.get_short_id() instead.', DeprecationWarning) return self.slug.upper() @property def color(self): if self.forced_color is not None: return '#%s' % self.forced_color return get_hashed_color(self.callsign or self.slug) @property def member_set(self): from sentry.models import OrganizationMember return self.organization.member_set.filter( id__in=OrganizationMember.objects.filter( organizationmemberteam__is_active=True, organizationmemberteam__team__in=self.teams.all(), ).values('id'), user__is_active=True, ).distinct() def has_access(self, user, access=None): from sentry.models import AuthIdentity, OrganizationMember warnings.warn('Project.has_access is deprecated.', DeprecationWarning) queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) try: member = queryset.get() except OrganizationMember.DoesNotExist: return False try: auth_identity = AuthIdentity.objects.get( auth_provider__organization=self.organization_id, user=member.user_id, ) except AuthIdentity.DoesNotExist: return True return auth_identity.is_valid(member) def get_audit_log_data(self): return { 'id': self.id, 'slug': self.slug, 'name': self.name, 'status': self.status, 'public': self.public, } def get_full_name(self): return self.slug def get_notification_recipients(self, user_option): from sentry.models import UserOption alert_settings = dict( (o.user_id, int(o.value)) for o in UserOption.objects.filter( project=self, key=user_option, ) ) disabled = set(u for u, v in six.iteritems(alert_settings) if v == 0) member_set = set( self.member_set.exclude( user__in=disabled, ).values_list('user', flat=True) ) # determine members default settings members_to_check = set(u for u in member_set if u not in alert_settings) if members_to_check: disabled = set( ( uo.user_id for uo in UserOption.objects.filter( key='subscribe_by_default', user__in=members_to_check, ) if uo.value == '0' ) ) member_set = [x for x in member_set if x not in disabled] return member_set def get_mail_alert_subscribers(self): user_ids = self.get_notification_recipients('mail:alert') if not user_ids: return [] from sentry.models import User return list(User.objects.filter(id__in=user_ids)) def is_user_subscribed_to_mail_alerts(self, user): from sentry.models import UserOption is_enabled = UserOption.objects.get_value(user, 'mail:alert', project=self) if is_enabled is None: is_enabled = UserOption.objects.get_value(user, 'subscribe_by_default', '1') == '1' else: is_enabled = bool(is_enabled) return is_enabled def transfer_to(self, team=None, organization=None): # TODO(jess): remove this when new-teams is live for everyone # only support passing team or org not both assert not (team and organization) # NOTE: this will only work properly if the new team is in a different # org than the existing one, which is currently the only use case in # production # TODO(jess): refactor this to make it an org transfer only from sentry.models import ( Environment, EnvironmentProject, ProjectTeam, ReleaseProject, ReleaseProjectEnvironment, Rule, ) if organization is None: organization = team.organization old_org_id = self.organization_id org_changed = old_org_id != organization.id self.organization = organization try: with transaction.atomic(): self.update( organization=organization, ) except IntegrityError: slugify_instance(self, self.name, organization=organization) self.update( slug=self.slug, organization=organization, ) # Both environments and releases are bound at an organization level. # Due to this, when you transfer a project into another org, we have to # handle this behavior somehow. We really only have two options here: # * Copy over all releases/environments into the new org and handle de-duping # * Delete the bindings and let them reform with new data. # We're generally choosing to just delete the bindings since new data # flowing in will recreate links correctly. The tradeoff is that # historical data is lost, but this is a compromise we're willing to # take and a side effect of allowing this feature. There are exceptions # to this however, such as rules, which should maintain their # configuration when moved across organizations. if org_changed: for model in ReleaseProject, ReleaseProjectEnvironment, EnvironmentProject: model.objects.filter( project_id=self.id, ).delete() # this is getting really gross, but make sure there aren't lingering associations # with old orgs or teams ProjectTeam.objects.filter(project=self, team__organization_id=old_org_id).delete() rules_by_environment_id = defaultdict(set) for rule_id, environment_id in Rule.objects.filter( project_id=self.id, environment_id__isnull=False).values_list('id', 'environment_id'): rules_by_environment_id[environment_id].add(rule_id) environment_names = dict( Environment.objects.filter( id__in=rules_by_environment_id, ).values_list('id', 'name') ) for environment_id, rule_ids in rules_by_environment_id.items(): Rule.objects.filter(id__in=rule_ids).update( environment_id=Environment.get_or_create( self, environment_names[environment_id], ).id, ) # ensure this actually exists in case from team was null if team is not None: self.add_team(team) def add_team(self, team): try: with transaction.atomic(): ProjectTeam.objects.create(project=self, team=team) except IntegrityError: return False else: return True def remove_team(self, team): ProjectTeam.objects.filter( project=self, team=team, ).delete() def get_security_token(self): lock = locks.get(self.get_lock_key(), duration=5) with TimedRetryPolicy(10)(lock.acquire): security_token = self.get_option('sentry:token', None) if security_token is None: security_token = uuid1().hex self.update_option('sentry:token', security_token) return security_token def get_lock_key(self): return 'project_token:%s' % self.id
class ProjectKey(Model): project = FlexibleForeignKey('sentry.Project', related_name='key_set') label = models.CharField(max_length=64, blank=True, null=True) public_key = models.CharField(max_length=32, unique=True, null=True) secret_key = models.CharField(max_length=32, unique=True, null=True) roles = BitField( flags=( # access to post events to the store endpoint ('store', 'Event API access'), # read/write access to rest API ('api', 'Web API access'), ), default=['store']) status = BoundedPositiveIntegerField(default=0, choices=( (ProjectKeyStatus.ACTIVE, _('Active')), (ProjectKeyStatus.INACTIVE, _('Inactive')), ), db_index=True) date_added = models.DateTimeField(default=timezone.now, null=True) objects = BaseManager(cache_fields=( 'public_key', 'secret_key', )) # support legacy project keys in API scopes = ( 'project:read', 'project:write', 'project:delete', 'event:read', 'event:write', 'event:delete', ) class Meta: app_label = 'sentry' db_table = 'sentry_projectkey' __repr__ = sane_repr('project_id', 'public_key') def __unicode__(self): return six.text_type(self.public_key) @classmethod def generate_api_key(cls): return uuid4().hex @classmethod def from_dsn(cls, dsn): urlparts = urlparse(dsn) public_key = urlparts.username project_id = urlparts.path.rsplit('/', 1)[-1] try: return ProjectKey.objects.get( public_key=public_key, project=project_id, ) except ValueError: # ValueError would come from a non-integer project_id, # which is obviously a DoesNotExist. We catch and rethrow this # so anything downstream expecting DoesNotExist works fine raise ProjectKey.DoesNotExist( 'ProjectKey matching query does not exist.') @classmethod def get_default(cls, project): try: return cls.objects.filter(project=project, roles=cls.roles.store, status=ProjectKeyStatus.ACTIVE)[0] except IndexError: return None @property def is_active(self): return self.status == ProjectKeyStatus.ACTIVE def save(self, *args, **kwargs): if not self.public_key: self.public_key = ProjectKey.generate_api_key() if not self.secret_key: self.secret_key = ProjectKey.generate_api_key() if not self.label: self.label = petname.Generate(2, ' ').title() super(ProjectKey, self).save(*args, **kwargs) def get_dsn(self, domain=None, secure=True, public=False): if not public: key = '%s:%s' % (self.public_key, self.secret_key) url = settings.SENTRY_ENDPOINT else: key = self.public_key url = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT if url: urlparts = urlparse(url) else: urlparts = urlparse(options.get('system.url-prefix')) return '%s://%s@%s/%s' % ( urlparts.scheme, key, urlparts.netloc + urlparts.path, self.project_id, ) @property def dsn_private(self): return self.get_dsn(public=False) @property def dsn_public(self): return self.get_dsn(public=True) @property def csp_endpoint(self): endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT if not endpoint: endpoint = options.get('system.url-prefix') return '%s%s?sentry_key=%s' % ( endpoint, reverse('sentry-api-csp-report', args=[self.project_id]), self.public_key, ) def get_allowed_origins(self): from sentry.utils.http import get_origins return get_origins(self.project) def get_audit_log_data(self): return { 'label': self.label, 'public_key': self.public_key, 'secret_key': self.secret_key, 'roles': int(self.roles), 'status': self.status, } def get_scopes(self): return self.scopes
class OrganizationMember(Model): """ Identifies relationships between teams and users. Users listed as team members are considered to have access to all projects and could be thought of as team owners (though their access level may not) be set to ownership. """ __core__ = True organization = FlexibleForeignKey('sentry.Organization', related_name="member_set") user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name="sentry_orgmember_set") email = models.EmailField(null=True, blank=True) role = models.CharField( choices=roles.get_choices(), max_length=32, default=roles.get_default().id, ) flags = BitField(flags=( ('sso:linked', 'sso:linked'), ('sso:invalid', 'sso:invalid'), ), default=0) token = models.CharField(max_length=64, null=True, blank=True, unique=True) date_added = models.DateTimeField(default=timezone.now) has_global_access = models.BooleanField(default=True) teams = models.ManyToManyField('sentry.Team', blank=True, through='sentry.OrganizationMemberTeam') # Deprecated -- no longer used type = BoundedPositiveIntegerField(default=50, blank=True) class Meta: app_label = 'sentry' db_table = 'sentry_organizationmember' unique_together = ( ('organization', 'user'), ('organization', 'email'), ) __repr__ = sane_repr( 'organization_id', 'user_id', 'role', ) @transaction.atomic def save(self, *args, **kwargs): assert self.user_id or self.email, \ 'Must set user or email' super(OrganizationMember, self).save(*args, **kwargs) @property def is_pending(self): return self.user_id is None @property def legacy_token(self): checksum = md5() checksum.update(six.text_type(self.organization_id).encode('utf-8')) checksum.update(self.get_email().encode('utf-8')) checksum.update(force_bytes(settings.SECRET_KEY)) return checksum.hexdigest() def generate_token(self): return uuid4().hex + uuid4().hex def get_invite_link(self): if not self.is_pending: return None return absolute_uri( reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token or self.legacy_token, })) def send_invite_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization': self.organization, 'url': self.get_invite_link(), } msg = MessageBuilder( subject='Join %s in using Sentry' % self.organization.name, template='sentry/emails/member-invite.txt', html_template='sentry/emails/member-invite.html', type='organization.invite', context=context, ) try: msg.send_async([self.get_email()]) except Exception as e: logger = get_logger(name='sentry.mail') logger.exception(e) def send_sso_link_email(self, actor, provider): from sentry.utils.email import MessageBuilder link_args = {'organization_slug': self.organization.slug} context = { 'organization': self.organization, 'actor': actor, 'provider': provider, 'url': absolute_uri(reverse('sentry-auth-organization', kwargs=link_args)), } msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name, ), template='sentry/emails/auth-link-identity.txt', html_template='sentry/emails/auth-link-identity.html', type='organization.auth_link', context=context, ) msg.send_async([self.get_email()]) def send_sso_unlink_email(self, actor, provider): from sentry.utils.email import MessageBuilder from sentry.models import LostPasswordHash email = self.get_email() recover_uri = u'{path}?{query}'.format( path=reverse('sentry-account-recover'), query=urlencode({'email': email}), ) # Nothing to send if this member isn't associated to a user if not self.user_id: return context = { 'email': email, 'recover_url': absolute_uri(recover_uri), 'has_password': self.user.password, 'organization': self.organization, 'actor': actor, 'provider': provider, } if not self.user.password: password_hash = LostPasswordHash.for_user(self.user) context['set_password_url'] = password_hash.get_absolute_url( mode='set_password') msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name, ), template='sentry/emails/auth-sso-disabled.txt', html_template='sentry/emails/auth-sso-disabled.html', type='organization.auth_sso_disabled', context=context, ) msg.send_async([email]) def get_display_name(self): if self.user_id: return self.user.get_display_name() return self.email def get_label(self): if self.user_id: return self.user.get_label() return self.email or self.id def get_email(self): if self.user_id and self.user.email: return self.user.email return self.email def get_avatar_type(self): if self.user_id: return self.user.get_avatar_type() return 'letter_avatar' def get_audit_log_data(self): from sentry.models import Team teams = list( Team.objects.filter(id__in=OrganizationMemberTeam.objects.filter( organizationmember=self, is_active=True, ).values_list('team', flat=True)).values('id', 'slug')) return { 'email': self.get_email(), 'user': self.user_id, 'teams': [t['id'] for t in teams], 'teams_slugs': [t['slug'] for t in teams], 'has_global_access': self.has_global_access, 'role': self.role, } def get_teams(self): from sentry.models import Team if roles.get(self.role).is_global: return self.organization.team_set.all() return Team.objects.filter( id__in=OrganizationMemberTeam.objects.filter( organizationmember=self, is_active=True, ).values('team')) def get_scopes(self): return roles.get(self.role).scopes
class Organization(Model): """ An organization represents a group of individuals which maintain ownership of projects. """ __core__ = True name = models.CharField(max_length=64) slug = models.SlugField(unique=True) status = BoundedPositiveIntegerField(choices=( (OrganizationStatus.VISIBLE, _('Visible')), (OrganizationStatus.PENDING_DELETION, _('Pending Deletion')), (OrganizationStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')), ), default=OrganizationStatus.VISIBLE) date_added = models.DateTimeField(default=timezone.now) members = models.ManyToManyField(settings.AUTH_USER_MODEL, through='sentry.OrganizationMember', related_name='org_memberships') default_role = models.CharField( choices=roles.get_choices(), max_length=32, default=roles.get_default().id, ) flags = BitField(flags=( ('allow_joinleave', 'Allow members to join and leave teams without requiring approval.'), ('enhanced_privacy', 'Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.' ), ('disable_shared_issues', 'Disable sharing of limited details on issues to anonymous users.'), ('early_adopter', 'Enable early adopter status, gaining access to features prior to public release.' ), ), default=1) objects = OrganizationManager(cache_fields=( 'pk', 'slug', )) class Meta: app_label = 'sentry' db_table = 'sentry_organization' __repr__ = sane_repr('owner_id', 'name', 'slug') @classmethod def get_default(cls): """ Return the organization used in single organization mode. """ return cls.objects.filter(status=OrganizationStatus.VISIBLE, )[0] def __unicode__(self): return u'%s (%s)' % (self.name, self.slug) def save(self, *args, **kwargs): if not self.slug: lock = locks.get('slug:organization', duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance(self, self.name, reserved=RESERVED_ORGANIZATION_SLUGS) super(Organization, self).save(*args, **kwargs) else: super(Organization, self).save(*args, **kwargs) def delete(self): if self.is_default: raise Exception('You cannot delete the the default organization.') return super(Organization, self).delete() @cached_property def is_default(self): if not settings.SENTRY_SINGLE_ORGANIZATION: return False return self == type(self).get_default() def has_access(self, user, access=None): queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) return queryset.exists() def get_audit_log_data(self): return { 'id': self.id, 'slug': self.slug, 'name': self.name, 'status': self.status, 'flags': self.flags, 'default_role': self.default_role, } def get_owners(self): from sentry.models import User return User.objects.filter( sentry_orgmember_set__role=roles.get_top_dog().id, sentry_orgmember_set__organization=self, is_active=True, ) def get_default_owner(self): if not hasattr(self, '_default_owner'): self._default_owner = self.get_owners()[0] return self._default_owner def has_single_owner(self): from sentry.models import OrganizationMember count = OrganizationMember.objects.filter( organization=self, role=roles.get_top_dog().id, user__isnull=False, user__is_active=True, )[:2].count() return count == 1 def merge_to(from_org, to_org): from sentry.models import (ApiKey, AuditLogEntry, Commit, OrganizationMember, OrganizationMemberTeam, Project, Release, ReleaseCommit, ReleaseEnvironment, ReleaseFile, Repository, Team) for from_member in OrganizationMember.objects.filter( organization=from_org): try: to_member = OrganizationMember.objects.get( organization=to_org, user=from_member.user, ) except OrganizationMember.DoesNotExist: from_member.update(organization=to_org) to_member = from_member else: qs = OrganizationMemberTeam.objects.filter( organizationmember=from_member, is_active=True, ).select_related() for omt in qs: OrganizationMemberTeam.objects.create_or_update( organizationmember=to_member, team=omt.team, defaults={ 'is_active': True, }, ) for team in Team.objects.filter(organization=from_org): try: with transaction.atomic(): team.update(organization=to_org) except IntegrityError: slugify_instance(team, team.name, organization=to_org) team.update( organization=to_org, slug=team.slug, ) for project in Project.objects.filter(organization=from_org): try: with transaction.atomic(): project.update(organization=to_org) except IntegrityError: slugify_instance(project, project.name, organization=to_org) project.update( organization=to_org, slug=project.slug, ) # TODO(jess): update this when adding unique constraint # on version, organization for releases for release in Release.objects.filter(organization=from_org): try: to_release = Release.objects.get(version=release.version, organization=to_org) except Release.DoesNotExist: Release.objects.filter(id=release.id).update( organization=to_org) else: Release.merge(to_release, [release]) for model in (ApiKey, AuditLogEntry, ReleaseFile): model.objects.filter( organization=from_org, ).update(organization=to_org) for model in (Commit, ReleaseCommit, ReleaseEnvironment, Repository): model.objects.filter(organization_id=from_org.id, ).update( organization_id=to_org.id) # TODO: Make these a mixin def update_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.set_value(self, *args, **kwargs) def get_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.get_value(self, *args, **kwargs) def delete_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.unset_value(self, *args, **kwargs) def send_delete_confirmation(self, audit_log_entry, countdown): from sentry import options from sentry.utils.email import MessageBuilder owners = self.get_owners() context = { 'organization': self, 'audit_log_entry': audit_log_entry, 'eta': timezone.now() + timedelta(seconds=countdown), 'url': absolute_uri( reverse( 'sentry-restore-organization', args=[self.slug], )), } MessageBuilder( subject='%sOrganization Queued for Deletion' % (options.get('mail.subject-prefix'), ), template='sentry/emails/org_delete_confirm.txt', html_template='sentry/emails/org_delete_confirm.html', type='org.confirm_delete', context=context, ).send_async([o.email for o in owners])
class Organization(Model): """ An organization represents a group of individuals which maintain ownership of projects. """ name = models.CharField(max_length=64) slug = models.SlugField(unique=True) status = BoundedPositiveIntegerField(choices=( (OrganizationStatus.VISIBLE, _('Visible')), (OrganizationStatus.PENDING_DELETION, _('Pending Deletion')), (OrganizationStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')), ), default=OrganizationStatus.VISIBLE) date_added = models.DateTimeField(default=timezone.now) members = models.ManyToManyField(settings.AUTH_USER_MODEL, through='sentry.OrganizationMember', related_name='org_memberships') default_role = models.CharField( choices=roles.get_choices(), max_length=32, default=roles.get_default().id, ) flags = BitField(flags=( ('allow_joinleave', 'Allow members to join and leave teams without requiring approval.'), ('enhanced_privacy', 'Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.' ), ('disable_shared_issues', 'Disable sharing of limited details on issues to anonymous users.'), ), default=1) objects = OrganizationManager(cache_fields=( 'pk', 'slug', )) class Meta: app_label = 'sentry' db_table = 'sentry_organization' __repr__ = sane_repr('owner_id', 'name', 'slug') @classmethod def get_default(cls): """ Return the organization used in single organization mode. """ return cls.objects.filter(status=OrganizationStatus.VISIBLE, )[0] def __unicode__(self): return u'%s (%s)' % (self.name, self.slug) def save(self, *args, **kwargs): if not self.slug: lock_key = 'slug:organization' with Lock(lock_key): slugify_instance(self, self.name, reserved=RESERVED_ORGANIZATION_SLUGS) super(Organization, self).save(*args, **kwargs) else: super(Organization, self).save(*args, **kwargs) def delete(self): if self.is_default: raise Exception('You cannot delete the the default organization.') return super(Organization, self).delete() @cached_property def is_default(self): if not settings.SENTRY_SINGLE_ORGANIZATION: return False return self == type(self).get_default() def has_access(self, user, access=None): queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) return queryset.exists() def get_audit_log_data(self): return { 'id': self.id, 'slug': self.slug, 'name': self.name, 'status': self.status, 'flags': self.flags, 'default_role': self.default_role, } def get_default_owner(self): if not hasattr(self, '_default_owner'): from sentry.models import User self._default_owner = User.objects.filter( sentry_orgmember_set__role=roles.get_top_dog().id, sentry_orgmember_set__organization=self, )[0] return self._default_owner def has_single_owner(self): from sentry.models import OrganizationMember count = OrganizationMember.objects.filter( organization=self, role='owner', user__isnull=False, ).count() return count == 1 def merge_to(from_org, to_org): from sentry.models import (ApiKey, AuditLogEntry, OrganizationMember, OrganizationMemberTeam, Project, Team) for from_member in OrganizationMember.objects.filter( organization=from_org): try: to_member = OrganizationMember.objects.get( organization=to_org, user=from_member.user, ) except OrganizationMember.DoesNotExist: from_member.update(organization=to_org) to_member = from_member else: qs = OrganizationMemberTeam.objects.filter( organizationmember=from_member, is_active=True, ).select_related() for omt in qs: OrganizationMemberTeam.objects.create_or_update( organizationmember=to_member, team=omt.team, defaults={ 'is_active': True, }, ) for team in Team.objects.filter(organization=from_org): try: with transaction.atomic(): team.update(organization=to_org) except IntegrityError: slugify_instance(team, team.name, organization=to_org) team.update( organization=to_org, slug=team.slug, ) for project in Project.objects.filter(organization=from_org): try: with transaction.atomic(): project.update(organization=to_org) except IntegrityError: slugify_instance(project, project.name, organization=to_org) project.update( organization=to_org, slug=project.slug, ) for model in (ApiKey, AuditLogEntry): model.objects.filter( organization=from_org, ).update(organization=to_org) # TODO: Make these a mixin def update_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.set_value(self, *args, **kwargs) def get_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.get_value(self, *args, **kwargs) def delete_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.unset_value(self, *args, **kwargs)
class Child(models.Model): GENDER_CHOICES = Choices( ("m", _("male")), ("f", _("female")), ("o", _("other")), ("na", _("prefer not to answer")), ) # Deprecating AGE_AT_BIRTH_CHOICES = Choices( ("na", _("Not sure or prefer not to answer")), ("<24", _("Under 24 weeks")), ("24", _("24 weeks")), ("25", _("25 weeks")), ("26", _("26 weeks")), ("27", _("27 weeks")), ("28", _("28 weeks")), ("29", _("29 weeks")), ("30", _("30 weeks")), ("31", _("31 weeks")), ("32", _("32 weeks")), ("33", _("33 weeks")), ("34", _("34 weeks")), ("35", _("35 weeks")), ("36", _("36 weeks")), ("37", _("37 weeks")), ("38", _("38 weeks")), ("39", _("39 weeks")), ("40>", _("40 or more weeks")), ) uuid = models.UUIDField(verbose_name="identifier", default=uuid.uuid4, unique=True, db_index=True) given_name = models.CharField(max_length=255) birthday = models.DateField(blank=True, null=True) gender = models.CharField(max_length=2, choices=GENDER_CHOICES) gestational_age_at_birth = models.PositiveSmallIntegerField( choices=GESTATIONAL_AGE_CHOICES, default=GESTATIONAL_AGE_CHOICES.no_answer, null=True, blank=True, ) additional_information = models.TextField(blank=True) deleted = models.BooleanField(default=False) former_lookit_profile_id = models.CharField(max_length=255, blank=True) existing_conditions = BitField(flags=CONDITIONS, default=0) languages_spoken = BitField(flags=LANGUAGES, default=0) user = models.ForeignKey( "accounts.User", related_name="children", related_query_name="children", on_delete=models. CASCADE # if deleting User, also delete associated Child - # although may not be possible depending on Responses already associated ) objects = BitfieldQuerySet.as_manager() def __str__(self): return f"<Child: {self.given_name}>" @property def age_at_birth(self): return GESTATIONAL_AGE_CHOICES[self.gestational_age_at_birth] @property def language_list(self): return " ".join([ langcode for langcode, boolean in self.languages_spoken.items() if boolean ]) @property def condition_list(self): return " ".join([ condition for condition, boolean in self.existing_conditions.items() if boolean ]) class Meta: permissions = [("can_view_all_children_in_analytics", "Can view all children in analytics")] ordering = ["-birthday"] class JSONAPIMeta: resource_name = "children" lookup_field = "uuid"
class TestModel(models.Model): flags = BitField(flags=(u"FLAG_0", u"FLAG_1", u"FLAG_2", u"FLAG_3"), default=(u"FLAG_1", u"FLAG_2"))
class ApiToken(Model): __core__ = True # users can generate tokens without being application-bound application = FlexibleForeignKey('sentry.ApiApplication', null=True) user = FlexibleForeignKey('sentry.User') token = models.CharField(max_length=64, unique=True, default=lambda: ApiToken.generate_token()) refresh_token = models.CharField(max_length=64, unique=True, null=True, default=lambda: ApiToken.generate_token()) expires_at = models.DateTimeField( null=True, default=lambda: timezone.now() + DEFAULT_EXPIRATION) scopes = BitField(flags=( ('project:read', 'project:read'), ('project:write', 'project:write'), ('project:admin', 'project:admin'), ('project:releases', 'project:releases'), ('team:read', 'team:read'), ('team:write', 'team:write'), ('team:admin', 'team:admin'), ('event:read', 'event:read'), ('event:write', 'event:write'), ('event:admin', 'event:admin'), ('org:read', 'org:read'), ('org:write', 'org:write'), ('org:admin', 'org:admin'), ('member:read', 'member:read'), ('member:write', 'member:write'), ('member:admin', 'member:admin'), )) scope_list = ArrayField(of=models.TextField) date_added = models.DateTimeField(default=timezone.now) objects = BaseManager(cache_fields=('token', )) class Meta: app_label = 'sentry' db_table = 'sentry_apitoken' __repr__ = sane_repr('user_id', 'token', 'application_id') def __unicode__(self): return six.text_type(self.token) @classmethod def generate_token(cls): return uuid4().hex + uuid4().hex @classmethod def from_grant(cls, grant): with transaction.atomic(): return cls.objects.create( application=grant.application, user=grant.user, scope_list=grant.get_scopes(), ) def is_expired(self): if not self.expires_at: return False return timezone.now() >= self.expires_at def get_audit_log_data(self): return { 'scopes': self.get_scopes(), } def get_scopes(self): if self.scope_list: return self.scope_list return [k for k, v in six.iteritems(self.scopes) if v] def has_scope(self, scope): return scope in self.get_scopes() def get_allowed_origins(self): if self.application: return self.application.get_allowed_origins() return () def refresh(self, expires_at=None): if expires_at is None: expires_at = timezone.now() + DEFAULT_EXPIRATION self.update( token=type(self).generate_token(), refresh_token=type(self).generate_token(), expires_at=expires_at, )
class OrganizationMember(Model): """ Identifies relationships between teams and users. Users listed as team members are considered to have access to all projects and could be thought of as team owners (though their access level may not) be set to ownership. """ organization = FlexibleForeignKey('sentry.Organization', related_name="member_set") user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name="sentry_orgmember_set") email = models.EmailField(null=True, blank=True) role = models.CharField( choices=roles.get_choices(), max_length=32, default=roles.get_default().id, ) flags = BitField(flags=( ('sso:linked', 'sso:linked'), ('sso:invalid', 'sso:invalid'), ), default=0) date_added = models.DateTimeField(default=timezone.now) has_global_access = models.BooleanField(default=True) counter = BoundedPositiveIntegerField(null=True, blank=True) teams = models.ManyToManyField('sentry.Team', blank=True, through='sentry.OrganizationMemberTeam') # Deprecated -- no longer used type = BoundedPositiveIntegerField(default=50, blank=True) class Meta: app_label = 'sentry' db_table = 'sentry_organizationmember' unique_together = ( ('organization', 'user'), ('organization', 'email'), ) __repr__ = sane_repr( 'organization_id', 'user_id', 'role', ) @transaction.atomic def save(self, *args, **kwargs): assert self.user_id or self.email, \ 'Must set user or email' super(OrganizationMember, self).save(*args, **kwargs) if not self.counter: self._set_counter() @transaction.atomic def delete(self, *args, **kwargs): super(OrganizationMember, self).delete(*args, **kwargs) if self.counter: self._unshift_counter() def _unshift_counter(self): assert self.counter OrganizationMember.objects.filter( organization=self.organization, counter__gt=self.counter, ).update(counter=F('counter') - 1, ) def _set_counter(self): assert self.id and not self.counter # XXX(dcramer): this isnt atomic, but unfortunately MySQL doesnt support # the subquery pattern we'd need self.update(counter=OrganizationMember.objects.filter( organization=self.organization, ).count(), ) @property def is_pending(self): return self.user_id is None @property def token(self): checksum = md5() for x in (str(self.organization_id), self.get_email(), settings.SECRET_KEY): checksum.update(x) return checksum.hexdigest() def send_invite_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization': self.organization, 'url': absolute_uri( reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token, })), } msg = MessageBuilder( subject='Join %s in using Sentry' % self.organization.name, template='sentry/emails/member-invite.txt', html_template='sentry/emails/member-invite.html', context=context, ) try: msg.send([self.get_email()]) except Exception as e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e) def send_sso_link_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization_name': self.organization.name, 'url': absolute_uri( reverse('sentry-auth-organization', kwargs={ 'organization_slug': self.organization.slug, })), } msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name, ), template='sentry/emails/auth-link-identity.txt', html_template='sentry/emails/auth-link-identity.html', context=context, ) msg.send_async([self.get_email()]) def get_display_name(self): if self.user_id: return self.user.get_display_name() return self.email def get_label(self): if self.user_id: return self.user.get_label() return self.email or self.id def get_email(self): if self.user_id: return self.user.email return self.email def get_avatar_type(self): if self.user_id: return self.user.get_avatar_type() def get_audit_log_data(self): from sentry.models import Team return { 'email': self.email, 'user': self.user_id, 'teams': list( Team.objects.filter( id__in=OrganizationMemberTeam.objects.filter( organizationmember=self, is_active=True, ).values_list('team', flat=True))), 'has_global_access': self.has_global_access, 'role': self.role, } def get_teams(self): from sentry.models import Team if roles.get(self.role).is_global: return self.organization.team_set.all() return Team.objects.filter( id__in=OrganizationMemberTeam.objects.filter( organizationmember=self, is_active=True, ).values('team')) def get_scopes(self): return roles.get(self.role).scopes def can_manage_member(self, member): return roles.can_manage(self.role, member.role)
class OrganizationMember(Model): """ Identifies relationships between teams and users. Users listed as team members are considered to have access to all projects and could be thought of as team owners (though their access level may not) be set to ownership. """ organization = FlexibleForeignKey('sentry.Organization', related_name="member_set") user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name="sentry_orgmember_set") email = models.EmailField(null=True, blank=True) type = BoundedPositiveIntegerField(choices=( (OrganizationMemberType.BOT, _('Bot')), (OrganizationMemberType.MEMBER, _('Member')), (OrganizationMemberType.ADMIN, _('Admin')), (OrganizationMemberType.OWNER, _('Owner')), ), default=OrganizationMemberType.MEMBER) flags = BitField(flags=( ('sso:linked', 'sso:linked'), ('sso:invalid', 'sso:invalid'), ), default=0) date_added = models.DateTimeField(default=timezone.now) has_global_access = models.BooleanField(default=True) teams = models.ManyToManyField('sentry.Team', blank=True) class Meta: app_label = 'sentry' db_table = 'sentry_organizationmember' unique_together = (('organization', 'user'), ('organization', 'email')) __repr__ = sane_repr('organization_id', 'user_id', 'type') def save(self, *args, **kwargs): assert self.user_id or self.email, \ 'Must set user or email' return super(OrganizationMember, self).save(*args, **kwargs) @property def is_pending(self): return self.user_id is None @property def token(self): checksum = md5() for x in (str(self.organization_id), self.get_email(), settings.SECRET_KEY): checksum.update(x) return checksum.hexdigest() @property def scopes(self): scopes = [] if self.type <= OrganizationMemberType.MEMBER: scopes.extend([ 'event:read', 'event:write', 'event:delete', 'org:read', 'project:read', 'team:read', ]) if self.type <= OrganizationMemberType.ADMIN: scopes.extend(['project:write', 'team:write']) if self.type <= OrganizationMemberType.OWNER: scopes.extend(['project:delete', 'team:delete']) if self.has_global_access: if self.type <= OrganizationMemberType.ADMIN: scopes.extend(['org:write']) if self.type <= OrganizationMemberType.OWNER: scopes.extend(['org:delete']) return scopes def send_invite_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization': self.organization, 'url': absolute_uri(reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token, })), } msg = MessageBuilder( subject='Invite to join organization: %s' % (self.organization.name,), template='sentry/emails/member_invite.txt', context=context, ) try: msg.send([self.get_email()]) except Exception as e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e) def send_sso_link_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization_name': self.organization.name, 'url': absolute_uri(reverse('sentry-auth-link-identity', kwargs={ 'organization_slug': self.organization.slug, })), } msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name,), template='sentry/emails/auth-link-identity.txt', html_template='sentry/emails/auth-link-identity.html', context=context, ) try: msg.send([self.get_email()]) except Exception as e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e) def get_display_name(self): if self.user_id: return self.user.get_display_name() return self.email def get_email(self): if self.user_id: return self.user.email return self.email def get_audit_log_data(self): return { 'email': self.email, 'user': self.user_id, 'teams': [t.id for t in self.teams.all()], 'has_global_access': self.has_global_access, }
class Game(models.Model): """Game model""" GAME_FLAGS = ( ('fully_libre', 'Fully libre'), ('open_engine', 'Open engine only'), ('free', 'Free'), ('freetoplay', 'Free-to-play'), ('pwyw', 'Pay what you want'), ('demo', 'Has a demo'), ) name = models.CharField(max_length=200) slug = models.SlugField(unique=True, blank=False) year = models.IntegerField(null=True, blank=True) platforms = models.ManyToManyField(Platform) genres = models.ManyToManyField(Genre) publisher = models.ForeignKey(Company, related_name='published_game', null=True, blank=True) developer = models.ForeignKey(Company, related_name='developed_game', null=True, blank=True) website = models.CharField(max_length=200, blank=True) icon = models.ImageField(upload_to='games/icons', blank=True) title_logo = models.ImageField(upload_to='games/banners', blank=True) description = models.TextField(blank=True) is_public = models.BooleanField("Published", default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) steamid = models.PositiveIntegerField(null=True, blank=True) gogid = models.CharField(max_length=200, blank=True) humblestoreid = models.CharField(max_length=200, blank=True) flags = BitField(flags=GAME_FLAGS) objects = GameManager() # pylint: disable=W0232, R0903 class Meta(object): ordering = ['name'] permissions = (('can_publish_game', "Can publish game"), ) def __unicode__(self): return self.name @staticmethod def autocomplete_search_fields(): return ("name__icontains", ) @property def banner_url(self): if self.title_logo: return reverse('get_banner', kwargs={'slug': self.slug}) @property def icon_url(self): if self.icon: return reverse('get_icon', kwargs={'slug': self.slug}) @property def flag_labels(self): """Return labels of active flags, suitable for display""" return [ self.flags.get_label(flag[0]) for flag in self.flags if flag[1] ] def has_installer(self): return self.installers.exists() or self.has_auto_installers() def has_auto_installers(self): return self.platforms.filter(default_installer__isnull=False).exists() def get_absolute_url(self): """Return the absolute url for a game""" return reverse("game_detail", kwargs={'slug': self.slug}) def download_steam_capsule(self): if self.title_logo or not self.steamid: return else: self.title_logo = ContentFile(steam.get_capsule(self.steamid), "%d.jpg" % self.steamid) def get_steam_logo(self, img_url): self.title_logo = ContentFile(steam.get_image(self.steamid, img_url), "%d.jpg" % self.steamid) def get_steam_icon(self, img_url): self.icon = ContentFile(steam.get_image(self.steamid, img_url), "%d.jpg" % self.steamid) def steam_support(self): """ Return the platform supported by Steam """ if not self.steamid: return False platforms = [p.slug for p in self.platforms.all()] if 'linux' in platforms: return 'linux' elif 'windows' in platforms: return 'windows' else: return True def get_default_installers(self): installers = [] for platform in self.platforms.all(): if platform.default_installer: installer = platform.default_installer installer['name'] = self.name installer['game_slug'] = self.slug installer['version'] = platform.name installer['slug'] = "-".join( (self.slug[:30], platform.slug[:20])) installer['platform'] = platform.slug installer['description'] = "" installer['published'] = True installer['auto'] = True installers.append(installer) return installers def check_for_submission(self): # Skip freshly created and unpublished objects if not self.pk or not self.is_public: return # Skip objects that were already published original = Game.objects.get(pk=self.pk) if original.is_public: return try: submission = GameSubmission.objects.get(game=self, accepted_at__isnull=True) except GameSubmission.DoesNotExist: pass else: submission.accept() def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name)[:50] self.download_steam_capsule() self.check_for_submission() return super(Game, self).save(*args, **kwargs)
class Project(Model, PendingDeletionMixin): from sentry.models.projectteam import ProjectTeam """ Projects are permission based namespaces which generally are the top level entry point for all data. """ __include_in_export__ = True slug = models.SlugField(null=True) name = models.CharField(max_length=200) forced_color = models.CharField(max_length=6, null=True, blank=True) organization = FlexibleForeignKey("sentry.Organization") teams = models.ManyToManyField("sentry.Team", related_name="teams", through=ProjectTeam) public = models.BooleanField(default=False) date_added = models.DateTimeField(default=timezone.now) status = BoundedPositiveIntegerField( default=0, choices=( (ObjectStatus.VISIBLE, _("Active")), (ObjectStatus.PENDING_DELETION, _("Pending Deletion")), (ObjectStatus.DELETION_IN_PROGRESS, _("Deletion in Progress")), ), db_index=True, ) # projects that were created before this field was present # will have their first_event field set to date_added first_event = models.DateTimeField(null=True) flags = BitField( flags=( ("has_releases", "This Project has sent release data"), ("has_issue_alerts_targeting", "This Project has issue alerts targeting"), ("has_transactions", "This Project has sent transactions"), ("has_alert_filters", "This Project has filters"), ("has_sessions", "This Project has sessions"), ), default=10, null=True, ) objects = ProjectManager(cache_fields=["pk"]) platform = models.CharField(max_length=64, null=True) class Meta: app_label = "sentry" db_table = "sentry_project" unique_together = (("organization", "slug"), ) __repr__ = sane_repr("team_id", "name", "slug") _rename_fields_on_pending_delete = frozenset(["slug"]) def __str__(self): return f"{self.name} ({self.slug})" def next_short_id(self): from sentry.models import Counter with sentry_sdk.start_span( op="project.next_short_id") as span, metrics.timer( "project.next_short_id"): span.set_data("project_id", self.id) span.set_data("project_slug", self.slug) return Counter.increment(self) def save(self, *args, **kwargs): if not self.slug: lock = locks.get("slug:project", duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance( self, self.name, organization=self.organization, reserved=RESERVED_PROJECT_SLUGS, max_length=50, ) super().save(*args, **kwargs) else: super().save(*args, **kwargs) self.update_rev_for_option() def get_absolute_url(self, params=None): url = f"/organizations/{self.organization.slug}/issues/" params = {} if params is None else params params["project"] = self.id if params: url = url + "?" + urlencode(params) return absolute_uri(url) def is_internal_project(self): for value in (settings.SENTRY_FRONTEND_PROJECT, settings.SENTRY_PROJECT): if str(self.id) == str(value) or str(self.slug) == str(value): return True return False # TODO: Make these a mixin def update_option(self, *args, **kwargs): return projectoptions.set(self, *args, **kwargs) def get_option(self, *args, **kwargs): return projectoptions.get(self, *args, **kwargs) def delete_option(self, *args, **kwargs): return projectoptions.delete(self, *args, **kwargs) def update_rev_for_option(self): return projectoptions.update_rev_for_option(self) @property def color(self): if self.forced_color is not None: return f"#{self.forced_color}" return get_hashed_color(self.slug.upper()) @property def member_set(self): """:returns a QuerySet of all Users that belong to this Project""" from sentry.models import OrganizationMember return self.organization.member_set.filter( id__in=OrganizationMember.objects.filter( organizationmemberteam__is_active=True, organizationmemberteam__team__in=self.teams.all(), ).values("id"), user__is_active=True, ).distinct() def has_access(self, user, access=None): from sentry.models import AuthIdentity, OrganizationMember warnings.warn("Project.has_access is deprecated.", DeprecationWarning) queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) try: member = queryset.get() except OrganizationMember.DoesNotExist: return False try: auth_identity = AuthIdentity.objects.get( auth_provider__organization=self.organization_id, user=member.user_id) except AuthIdentity.DoesNotExist: return True return auth_identity.is_valid(member) def get_audit_log_data(self): return { "id": self.id, "slug": self.slug, "name": self.name, "status": self.status, "public": self.public, } def get_full_name(self): return self.slug def transfer_to(self, team=None, organization=None): # NOTE: this will only work properly if the new team is in a different # org than the existing one, which is currently the only use case in # production # TODO(jess): refactor this to make it an org transfer only from sentry.incidents.models import AlertRule from sentry.models import ( Environment, EnvironmentProject, ProjectTeam, ReleaseProject, ReleaseProjectEnvironment, Rule, ) from sentry.models.actor import ACTOR_TYPES if organization is None: organization = team.organization old_org_id = self.organization_id org_changed = old_org_id != organization.id self.organization = organization try: with transaction.atomic(): self.update(organization=organization) except IntegrityError: slugify_instance(self, self.name, organization=organization, max_length=50) self.update(slug=self.slug, organization=organization) # Both environments and releases are bound at an organization level. # Due to this, when you transfer a project into another org, we have to # handle this behavior somehow. We really only have two options here: # * Copy over all releases/environments into the new org and handle de-duping # * Delete the bindings and let them reform with new data. # We're generally choosing to just delete the bindings since new data # flowing in will recreate links correctly. The tradeoff is that # historical data is lost, but this is a compromise we're willing to # take and a side effect of allowing this feature. There are exceptions # to this however, such as rules, which should maintain their # configuration when moved across organizations. if org_changed: for model in ReleaseProject, ReleaseProjectEnvironment, EnvironmentProject: model.objects.filter(project_id=self.id).delete() # this is getting really gross, but make sure there aren't lingering associations # with old orgs or teams ProjectTeam.objects.filter( project=self, team__organization_id=old_org_id).delete() rules_by_environment_id = defaultdict(set) for rule_id, environment_id in Rule.objects.filter( project_id=self.id, environment_id__isnull=False).values_list( "id", "environment_id"): rules_by_environment_id[environment_id].add(rule_id) environment_names = dict( Environment.objects.filter( id__in=rules_by_environment_id).values_list("id", "name")) for environment_id, rule_ids in rules_by_environment_id.items(): Rule.objects.filter(id__in=rule_ids).update( environment_id=Environment.get_or_create( self, environment_names[environment_id]).id) # ensure this actually exists in case from team was null if team is not None: self.add_team(team) # Remove alert owners not in new org alert_rules = AlertRule.objects.fetch_for_project(self).filter( owner_id__isnull=False) rules = Rule.objects.filter(owner_id__isnull=False, project=self) for rule in list(chain(alert_rules, rules)): actor = rule.owner if actor.type == ACTOR_TYPES["user"]: is_member = organization.member_set.filter( user=actor.resolve()).exists() if actor.type == ACTOR_TYPES["team"]: is_member = actor.resolve().organization_id == organization.id if not is_member: rule.update(owner=None) AlertRule.objects.fetch_for_project(self).update( organization=organization) def add_team(self, team): from sentry.models.projectteam import ProjectTeam try: with transaction.atomic(): ProjectTeam.objects.create(project=self, team=team) except IntegrityError: return False else: return True def remove_team(self, team): from sentry.incidents.models import AlertRule from sentry.models import Rule from sentry.models.projectteam import ProjectTeam ProjectTeam.objects.filter(project=self, team=team).delete() AlertRule.objects.fetch_for_project(self).filter( owner_id=team.actor_id).update(owner=None) Rule.objects.filter(owner_id=team.actor_id, project=self).update(owner=None) def get_security_token(self): lock = locks.get(self.get_lock_key(), duration=5) with TimedRetryPolicy(10)(lock.acquire): security_token = self.get_option("sentry:token", None) if security_token is None: security_token = uuid1().hex self.update_option("sentry:token", security_token) return security_token def get_lock_key(self): return f"project_token:{self.id}" def copy_settings_from(self, project_id): """ Copies project level settings of the inputted project - General Settings - ProjectTeams - Alerts Settings and Rules - EnvironmentProjects - ProjectOwnership Rules and settings - Project Inbound Data Filters Returns True if the settings have successfully been copied over Returns False otherwise """ from sentry.models import ( EnvironmentProject, ProjectOption, ProjectOwnership, ProjectTeam, Rule, ) model_list = [EnvironmentProject, ProjectOwnership, ProjectTeam, Rule] project = Project.objects.get(id=project_id) try: with transaction.atomic(): for model in model_list: # remove all previous project settings model.objects.filter(project_id=self.id).delete() # add settings from other project to self for setting in model.objects.filter(project_id=project_id): setting.pk = None setting.project_id = self.id setting.save() options = ProjectOption.objects.get_all_values(project=project) for key, value in options.items(): self.update_option(key, value) except IntegrityError as e: logging.exception( "Error occurred during copy project settings.", extra={ "error": str(e), "project_to": self.id, "project_from": project_id, }, ) return False return True @staticmethod def is_valid_platform(value): if not value or value == "other": return True return integration_doc_exists(value) def delete(self, **kwargs): from sentry.models import NotificationSetting # There is no foreign key relationship so we have to manually cascade. NotificationSetting.objects.remove_for_project(self) return super().delete(**kwargs)
class Organization(Model): """ An organization represents a group of individuals which maintain ownership of projects. """ name = models.CharField(max_length=64) slug = models.SlugField(unique=True) status = BoundedPositiveIntegerField(choices=( (OrganizationStatus.VISIBLE, _('Visible')), (OrganizationStatus.PENDING_DELETION, _('Pending Deletion')), (OrganizationStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')), ), default=OrganizationStatus.VISIBLE) date_added = models.DateTimeField(default=timezone.now) members = models.ManyToManyField(settings.AUTH_USER_MODEL, through='sentry.OrganizationMember', related_name='org_memberships') flags = BitField(flags=(( 'allow_joinleave', 'Allow members to join and leave teams without requiring approval.'), ), default=0) objects = OrganizationManager(cache_fields=( 'pk', 'slug', )) class Meta: app_label = 'sentry' db_table = 'sentry_organization' __repr__ = sane_repr('owner_id', 'name', 'slug') @classmethod def get_default(cls): """ Return the organization used in single organization mode. """ return cls.objects.filter(status=OrganizationStatus.VISIBLE, )[0] def __unicode__(self): return u'%s (%s)' % (self.name, self.slug) def save(self, *args, **kwargs): if not self.slug: lock_key = 'slug:organization' with Lock(lock_key): slugify_instance(self, self.name, reserved=RESERVED_ORGANIZATION_SLUGS) super(Organization, self).save(*args, **kwargs) else: super(Organization, self).save(*args, **kwargs) def delete(self): if self.is_default: raise Exception('You cannot delete the the default organization.') return super(Organization, self).delete() @cached_property def is_default(self): if not settings.SENTRY_SINGLE_ORGANIZATION: return False return self == type(self).get_default() def has_access(self, user, access=None): queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) return queryset.exists() def get_audit_log_data(self): return { 'slug': self.slug, 'name': self.name, 'status': self.status, 'flags': self.flags, } def get_default_owner(self): from sentry.models import OrganizationMemberType, User return User.objects.filter( sentry_orgmember_set__type=OrganizationMemberType.OWNER, sentry_orgmember_set__organization=self, )[0] def has_single_owner(self): from sentry.models import OrganizationMember, OrganizationMemberType count = OrganizationMember.objects.filter( organization=self, type=OrganizationMemberType.OWNER, has_global_access=True, user__isnull=False, ).count() return count == 1 def merge_to(from_org, to_org): from sentry.models import (ApiKey, AuditLogEntry, OrganizationMember, OrganizationMemberTeam, Project, Team) team_list = list(Team.objects.filter(organization=to_org, )) for from_member in OrganizationMember.objects.filter( organization=from_org): try: to_member = OrganizationMember.objects.get( organization=to_org, user=from_member.user, ) except OrganizationMember.DoesNotExist: from_member.update(organization=to_org) to_member = from_member if to_member.has_global_access: for team in team_list: OrganizationMemberTeam.objects.get_or_create( organizationmember=to_member, team=team, defaults={ 'is_active': False, }, ) for model in (Team, Project, ApiKey, AuditLogEntry): model.objects.filter( organization=from_org, ).update(organization=to_org)
from bitfield import BitField from enum import Enum, Flag reserved = BitField(31, 31) instr_field = BitField(26, 30) cond_field = BitField(22, 25) reg_target_field = BitField(18, 21) reg_src1_field = BitField(14, 17) reg_src2_field = BitField(10, 13) offset_field = BitField(0, 9) # Registers are numbered from 0 to 15, and have names # like r3, r15, etc. Two special registers have additional # names: r0 is called 'zero' because on the DM2020W it always # holds value 0, and r15 is called 'pc' because it is used to # hold the program counter. # NAMED_REGS = { "r0": 0, "zero": 0, "r1": 1, "r2": 2, "r3": 3, "r4": 4, "r5": 5, "r6": 6, "r7": 7, "r8": 8, "r9": 9, "r10": 10, "r11": 11,
class Image(models.Model): user = models.ForeignKey(User) gallery = models.ForeignKey(Gallery, related_name="images") uploaded = models.DateTimeField(auto_now_add=True) title = models.CharField(max_length=256, null=True, blank=True) tags = TaggableManager(blank=True) uuid = ShortUUIDField(db_index=True) original = models.ImageField(upload_to=set_image_name_on_upload, ) _view_mapping = { "view.3d.180": "view_3d_180", "view.3d": "view_3d_360", "view.3d.360": "view_3d_360", "view.3d.sphere": "view_3d_360", "view.sphere": "view_3d_360", "view.pano": "view_2d_pano", "view.pan": "view_2d_pano", "view.panorama": "view_2d_pano", "view.panoramic": "view_2d_pano", } view_flags = BitField(flags=( ('view_3d_180', '180 Degrees 3D'), ('view_3d_360', '360 Degrees 3D'), ('view_2d_pano', 'Panoramic'), ), null=True) # for multiple tags we use the first view_default = models.CharField(max_length=32, null=True, blank=True) def self_uuid(self): return self.uuid full_fixed = ImageSpecField( source="original", processors=[Transpose()], format="JPEG", ) bigger = ImageSpecField( source="full_fixed", processors=[ResizeToCover(1440, 1080, upscale=False)], format="JPEG", options={ 'quality': 80, 'prefix': "b" }) default = ImageSpecField( source="full_fixed", processors=[ResizeToCover(720, 540, upscale=False)], format="JPEG", options={ 'quality': 80, 'prefix': "d" }) preview = ImageSpecField(source="full_fixed", processors=[SmartResize(320, 240)], format="JPEG", options={ 'quality': 80, 'prefix': "p" }) thumb = ImageSpecField( source="full_fixed", processors=[SmartResize(160, 120)], format="JPEG", options={ 'quality': 60, 'prefix': "t" }, ) tiny_thumb = ImageSpecField( source="full_fixed", processors=[SmartResize(80, 60)], format="JPEG", options={ 'quality': 40, 'prefix': "tt" }, ) AVAIL_SIZES = [ "full_fixed", "bigger", "default", "preview", "thumb", "tiny_thumb" ] AVAIL_INTS = [0, 1, 2, 3, 4, 5] exif_data = models.ManyToManyField("EXIFEntry", blank=True) exif_timestamp = models.DateTimeField(null=True, blank=True) @property def uuid_as_b64(self): return base64.b64encode(self.uuid) def query_exif(self, only_when_empty=True, do_empty=False): image = IMG.open(self.original) if do_empty: self.exif_data.clear() if only_when_empty: do = not self.exif_data.exists() else: do = True if do: try: exif_raw = image._getexif() except: # no usable exif return # I guess this deals with the compactness, so it needs to be decoded? if exif_raw: exif_decoded = { TAGS.get(k): v for k, v in exif_raw.iteritems() } out = [] for key, value in exif_decoded.iteritems(): ek, ck = ExifKey.objects.get_or_create(key=key) ev, cv = ExifValue.objects.get_or_create(value=value) ee, ce = EXIFEntry.objects.get_or_create(key=ek, value=ev) self.exif_data.add(ee) if key == "DateTime": value_stf = datetime.strptime(value, "%Y:%m:%d %H:%M:%S") self.exif_timestamp = value_stf self.save() else: pass # no exif def cached_full_fixed(self): generator = ImageCacheFile(self.full_fixed) return generator.generate() def cached_bigger(self): generator = ImageCacheFile(self.bigger) return generator.generate() def cached_default(self): generator = ImageCacheFile(self.default) return generator.generate() def cached_preview(self): generator = ImageCacheFile(self.preview) return generator.generate() def cached_thumb(self): generator = ImageCacheFile(self.thumb) return generator.generate() def cached_tiny_thumb(self): generator = ImageCacheFile(self.tiny_thumb) return generator.generate() def get_next_by_gallery_ordering(self): gallery = self.gallery gallery_sort = gallery.display_sort_string if gallery_sort == ['uploaded']: try: return self.get_next_by_uploaded(gallery=gallery) except: return None if gallery_sort == ['-uploaded']: try: return self.get_previous_by_uploaded(gallery=gallery) except: return None # todo add EXIF based sorting def get_prev_by_gallery_ordering(self): gallery = self.gallery gallery_sort = gallery.display_sort_string if gallery_sort == ['uploaded']: try: return self.get_previous_by_uploaded(gallery=gallery) except: return None if gallery_sort == ['-uploaded']: try: return self.get_next_by_uploaded(gallery=gallery) except: return None
class Game(models.Model): """Game model""" GAME_FLAGS = ( ("fully_libre", "Fully libre"), ("open_engine", "Open engine only"), ("free", "Free"), ("freetoplay", "Free-to-play"), ("pwyw", "Pay what you want"), ("demo", "Has a demo"), ("protected", "Installer modification is restricted"), ) # These model fields are editable by the user TRACKED_FIELDS = [ "name", "year", "platforms", "genres", "publisher", "developer", "website", "description", "title_logo" ] ICON_PATH = os.path.join(settings.MEDIA_ROOT, "game-icons/128") BANNER_PATH = os.path.join(settings.MEDIA_ROOT, "game-banners/184") name = models.CharField(max_length=200) slug = models.SlugField(unique=True, null=True, blank=True) year = models.IntegerField(null=True, blank=True) platforms = models.ManyToManyField(Platform, blank=True) genres = models.ManyToManyField(Genre, blank=True) publisher = models.ForeignKey( Company, related_name="published_game", null=True, blank=True, on_delete=models.SET_NULL, ) developer = models.ForeignKey( Company, related_name="developed_game", null=True, blank=True, on_delete=models.SET_NULL, ) website = models.CharField(max_length=200, blank=True) icon = models.ImageField(upload_to="uploads/icons", blank=True) title_logo = models.ImageField(upload_to="uploads/banners", blank=True) description = models.TextField(blank=True) is_public = models.BooleanField("Published", default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) steamid = models.PositiveIntegerField(null=True, blank=True) gogslug = models.CharField(max_length=200, blank=True) gogid = models.PositiveIntegerField(null=True, blank=True) humblestoreid = models.CharField(max_length=200, blank=True) flags = BitField(flags=GAME_FLAGS) popularity = models.IntegerField(default=0) provider_games = models.ManyToManyField(ProviderGame, related_name="games", blank=True) # Indicates whether this data row is a changeset for another data row. # If so, this attribute is not NULL and the value is the ID of the # corresponding data row change_for = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE) objects = GameManager() class Meta: """Model configuration""" ordering = ["name"] permissions = (("can_publish_game", "Can publish game"), ) @classmethod def valid_fields(cls): """Return a list of valid field names for the model""" return [f.name for f in cls._meta.fields] def __str__(self): if self.change_for is None: return self.name return "[Changes for] " + self.change_for.name @staticmethod def autocomplete_search_fields(): """Autocomplete fields used in the Django admin""" return ("name__icontains", ) @property def humbleid(self): """Humble Bundle ID, different from humblestoreid which is the store page ID for Humble Bundle """ gog_slugs = self.provider_games.filter( provider__name="HUMBLE").values_list("slug", flat=True) if gog_slugs: return gog_slugs[0] return "" @property def user_count(self): """How many users have the game in their libraries""" return self.libraries.count() @property def website_url(self): """Returns self.website guaranteed to be a valid URI""" if not self.website: return None # Fall back to http if no protocol specified (cannot assume that https will work) has_protocol = "://" in self.website return "http://" + self.website if not has_protocol else self.website @property def website_url_hr(self): """Returns a human readable website URL (stripped protocols and trailing slashes)""" if not self.website: return None return self.website.split("https:", 1)[-1].split("http:", 1)[-1].strip("/") @property def banner_url(self): """Return URL for the game banner""" if self.title_logo: return reverse("get_banner", kwargs={"slug": self.slug}) return "" @property def icon_url(self): """Return URL for the game icon""" if self.icon: return reverse("get_icon", kwargs={"slug": self.slug}) return "" @property def flag_labels(self): """Return labels of active flags, suitable for display""" # pylint: disable=E1133; self.flags *is* iterable return [ self.flags.get_label(flag[0]) for flag in self.flags if flag[1] ] def get_change_model(self): """Returns a dictionary which can be used as initial value in forms""" return { "name": self.name, "year": self.year, "platforms": [x.id for x in list(self.platforms.all())], "genres": [x.id for x in list(self.genres.all())], # The Select2 dropdowns want ids instead of complete models "publisher": self.publisher.id if self.publisher else None, "developer": self.developer.id if self.developer else None, "website": self.website, "description": self.description, "title_logo": self.title_logo, } def get_changes(self): """Returns a dictionary of the changes""" changes = [] # From the considered fields, only those who differ will be returned for entry in self.TRACKED_FIELDS: old_value = getattr(self.change_for, entry) new_value = getattr(self, entry) # M2M relations to string if entry in ["platforms", "genres"]: old_value = ", ".join("[{0}]".format(str(x)) for x in list(old_value.all())) new_value = ", ".join("[{0}]".format(str(x)) for x in list(new_value.all())) if old_value != new_value: changes.append((entry, old_value, new_value)) return changes def apply_changes(self, change_set): """Applies user-suggested changes to this model""" self.name = change_set.name self.year = change_set.year self.platforms.set(change_set.platforms.all()) self.genres.set(change_set.genres.all()) self.publisher = change_set.publisher self.developer = change_set.developer self.website = change_set.website self.description = change_set.description self.title_logo = change_set.title_logo def has_installer(self): """Return whether this game has an installer""" return self.installers.exists() or self.has_auto_installers() def has_auto_installers(self): """Return whether this game has auto-generated installers""" return self.platforms.filter(default_installer__isnull=False).exists() def get_absolute_url(self): """Return the absolute url for a game""" if self.change_for: slug = self.change_for.slug else: slug = self.slug return reverse("game_detail", kwargs={"slug": slug}) def precache_media(self): """Prerenders thumbnails so we can host them as static files""" icon_path = os.path.join(settings.MEDIA_ROOT, self.icon.name) if self.icon.name and os.path.exists(icon_path): self.precache_icon() banner_path = os.path.join(settings.MEDIA_ROOT, self.title_logo.name) if self.title_logo.name and os.path.exists(banner_path): self.precache_banner() def precache_icon(self): """Render the icon and place it in the icons folder""" dest_file = os.path.join(self.ICON_PATH, "%s.png" % self.slug) if os.path.exists(dest_file): return thumbnail = get_thumbnail(self.icon, settings.ICON_SIZE, crop="center", format="PNG") shutil.copy(os.path.join(settings.MEDIA_ROOT, thumbnail.name), dest_file) def precache_banner(self): """Render the icon and place it in the banners folder""" dest_file = os.path.join(self.BANNER_PATH, "%s.jpg" % self.slug) if os.path.exists(dest_file): return thumbnail = get_thumbnail(self.title_logo, settings.BANNER_SIZE, crop="center") shutil.copy(os.path.join(settings.MEDIA_ROOT, thumbnail.name), dest_file) def set_logo_from_steam(self): """Fetch the banner from Steam and use it for the game""" if self.title_logo or not self.steamid: return self.title_logo = ContentFile(steam.get_capsule(self.steamid), "%s.jpg" % self.steamid) def set_logo_from_steam_api(self, img_url): """Sets the game banner from the Steam API URLs""" self.title_logo = ContentFile(steam.get_image(self.steamid, img_url), "%s.jpg" % self.steamid) def set_icon_from_steam_api(self, img_url): """Sets the game icon from the Steam API URLs""" self.icon = ContentFile(steam.get_image(self.steamid, img_url), "%s.jpg" % self.steamid) def set_logo_from_gog(self, gog_game): """Sets the game logo from the data retrieved from GOG""" if self.title_logo or not self.gogid: return self.title_logo = ContentFile(gog.get_logo(gog_game), "gog-%s.jpg" % self.gogid) def steam_support(self): """ Return the platform supported by Steam """ if not self.steamid: return False platforms = [p.slug for p in self.platforms.all()] if "linux" in platforms: return "linux" if "windows" in platforms: return "windows" return True def get_default_installers(self): """Return all auto-installers for this game's platforms""" auto_installers = [] for platform in self.platforms.all(): if platform.default_installer: installer = platform.default_installer installer["name"] = self.name installer["game_slug"] = self.slug installer["version"] = platform.name installer["slug"] = "-".join( (self.slug[:30], platform.slug[:20])) installer["platform"] = platform.slug installer["description"] = "" installer["published"] = True installer["auto"] = True auto_installers.append(installer) return auto_installers def check_for_submission(self): """What? This saves submissions on save? Why? This is fully wrong. The name itself is a huge red flag since nothing is checked and this method has side effects. """ # Skip freshly created and unpublished objects if not self.pk or not self.is_public: return # Skip objects that were already published original = Game.objects.get(pk=self.pk) if original.is_public: return try: submission = GameSubmission.objects.get(game=self, accepted_at__isnull=True) except GameSubmission.DoesNotExist: pass else: submission.accept() def save(self, force_insert=False, force_update=False, using=None, update_fields=None): # Only create slug etc. if this is a game submission, no change submission if not self.change_for: if not self.slug: self.slug = slugify(self.name)[:50] if not self.slug: raise ValueError("Can't generate a slug for name %s" % self.name) self.set_logo_from_steam() self.check_for_submission() super(Game, self).save( force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields, ) # Not ideal to have this here since this can generate disk IO activity # Not a problem though, we want to discourage mass updates for games # since that would DDOS the site. self.precache_media()
class Game(models.Model): """Game model""" GAME_FLAGS = ( ('fully_libre', 'Fully libre'), ('open_engine', 'Open engine only'), ('free', 'Free'), ('freetoplay', 'Free-to-play'), ('pwyw', 'Pay what you want'), ('demo', 'Has a demo'), ('protected', 'Installer modification is restricted'), ) name = models.CharField(max_length=200) slug = models.SlugField(unique=True, null=True, blank=True) year = models.IntegerField(null=True, blank=True) platforms = models.ManyToManyField(Platform) genres = models.ManyToManyField(Genre) publisher = models.ForeignKey(Company, related_name='published_game', null=True, blank=True, on_delete=models.SET_NULL) developer = models.ForeignKey(Company, related_name='developed_game', null=True, blank=True, on_delete=models.SET_NULL) website = models.CharField(max_length=200, blank=True) icon = models.ImageField(upload_to='games/icons', blank=True) title_logo = models.ImageField(upload_to='games/banners', blank=True) description = models.TextField(blank=True) is_public = models.BooleanField("Published", default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) steamid = models.PositiveIntegerField(null=True, blank=True) gogslug = models.CharField(max_length=200, blank=True) gogid = models.PositiveIntegerField(null=True, unique=True) humblestoreid = models.CharField(max_length=200, blank=True) flags = BitField(flags=GAME_FLAGS) # Indicates whether this data row is a changeset for another data row. # If so, this attribute is not NULL and the value is the ID of the # corresponding data row change_for = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE) objects = GameManager() # pylint: disable=W0232, R0903 class Meta(object): ordering = ['name'] permissions = (('can_publish_game', "Can publish game"), ) def __str__(self): if self.change_for is None: return self.name return '[Changes for] ' + self.change_for.name @staticmethod def autocomplete_search_fields(): return ("name__icontains", ) @property def website_url(self): """Returns self.website guaranteed to be a valid URI""" if not self.website: return None # Fall back to http if no protocol specified (cannot assume that https will work) has_protocol = '://' in self.website return 'http://' + self.website if not has_protocol else self.website @property def website_url_hr(self): """Returns a human readable website URL (stripped protocols and trailing slashes)""" if not self.website: return None return (self.website.split('https:', 1)[-1].split('http:', 1)[-1].strip('/')) @property def banner_url(self): if self.title_logo: return reverse('get_banner', kwargs={'slug': self.slug}) @property def icon_url(self): if self.icon: return reverse('get_icon', kwargs={'slug': self.slug}) @property def flag_labels(self): """Return labels of active flags, suitable for display""" # pylint: disable=E1133; self.flags *is* iterable return [ self.flags.get_label(flag[0]) for flag in self.flags if flag[1] ] def get_change_model(self): """Returns a dictionary which can be used as initial value in forms""" return { 'name': self.name, 'year': self.year, 'website': self.website, 'description': self.description, 'platforms': [x.id for x in list(self.platforms.all())], 'genres': [x.id for x in list(self.genres.all())] } def get_changes(self): """Returns a dictionary of the changes""" changes = [] considered_entries = [ 'name', 'year', 'platforms', 'genres', 'website', 'description' ] # From the considered fields, only those who differ will be returned for entry in considered_entries: old_value = getattr(self.change_for, entry) new_value = getattr(self, entry) # M2M relations to string if entry in ['platforms', 'genres']: old_value = ', '.join('[{0}]'.format(str(x)) for x in list(old_value.all())) new_value = ', '.join('[{0}]'.format(str(x)) for x in list(new_value.all())) if old_value != new_value: changes.append((entry, old_value, new_value)) return changes def apply_changes(self, change_set): """Applies user-suggested changes to this model""" self.name = change_set.name self.year = change_set.year self.platforms.set(change_set.platforms.all()) self.genres.set(change_set.genres.all()) self.website = change_set.website self.description = change_set.description def has_installer(self): return self.installers.exists() or self.has_auto_installers() def has_auto_installers(self): return self.platforms.filter(default_installer__isnull=False).exists() def get_absolute_url(self): """Return the absolute url for a game""" if self.change_for: slug = self.change_for.slug else: slug = self.slug return reverse("game_detail", kwargs={'slug': slug}) def download_steam_capsule(self): if self.title_logo or not self.steamid: return else: self.title_logo = ContentFile(steam.get_capsule(self.steamid), "%d.jpg" % self.steamid) def get_steam_logo(self, img_url): self.title_logo = ContentFile(steam.get_image(self.steamid, img_url), "%d.jpg" % self.steamid) def get_steam_icon(self, img_url): self.icon = ContentFile(steam.get_image(self.steamid, img_url), "%d.jpg" % self.steamid) def steam_support(self): """ Return the platform supported by Steam """ if not self.steamid: return False platforms = [p.slug for p in self.platforms.all()] if 'linux' in platforms: return 'linux' elif 'windows' in platforms: return 'windows' return True def get_default_installers(self): installers = [] for platform in self.platforms.all(): if platform.default_installer: installer = platform.default_installer installer['name'] = self.name installer['game_slug'] = self.slug installer['version'] = platform.name installer['slug'] = "-".join( (self.slug[:30], platform.slug[:20])) installer['platform'] = platform.slug installer['description'] = "" installer['published'] = True installer['auto'] = True installers.append(installer) return installers def check_for_submission(self): # Skip freshly created and unpublished objects if not self.pk or not self.is_public: return # Skip objects that were already published original = Game.objects.get(pk=self.pk) if original.is_public: return try: submission = GameSubmission.objects.get(game=self, accepted_at__isnull=True) except GameSubmission.DoesNotExist: pass else: submission.accept() def save(self, force_insert=False, force_update=False, using=None, update_fields=None): # Only create slug etc. if this is a game submission, no change submission if not self.change_for: if not self.slug: self.slug = slugify(self.name)[:50] if not self.slug: raise ValueError("Can't generate a slug for name %s" % self.name) self.download_steam_capsule() self.check_for_submission() return super(Game, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
class ApiKey(Model): __core__ = True organization = FlexibleForeignKey("sentry.Organization", related_name="key_set") label = models.CharField(max_length=64, blank=True, default="Default") key = models.CharField(max_length=32, unique=True) scopes = BitField( flags=( ("project:read", "project:read"), ("project:write", "project:write"), ("project:admin", "project:admin"), ("project:releases", "project:releases"), ("team:read", "team:read"), ("team:write", "team:write"), ("team:admin", "team:admin"), ("event:read", "event:read"), ("event:write", "event:write"), ("event:admin", "event:admin"), ("org:read", "org:read"), ("org:write", "org:write"), ("org:admin", "org:admin"), ("member:read", "member:read"), ("member:write", "member:write"), ("member:admin", "member:admin"), ) ) scope_list = ArrayField(of=models.TextField) status = BoundedPositiveIntegerField( default=0, choices=((ApiKeyStatus.ACTIVE, _("Active")), (ApiKeyStatus.INACTIVE, _("Inactive"))), db_index=True, ) date_added = models.DateTimeField(default=timezone.now) allowed_origins = models.TextField(blank=True, null=True) objects = BaseManager(cache_fields=("key",)) class Meta: app_label = "sentry" db_table = "sentry_apikey" __repr__ = sane_repr("organization_id", "key") def __str__(self): return str(self.key) @classmethod def generate_api_key(cls): return uuid4().hex @property def is_active(self): return self.status == ApiKeyStatus.ACTIVE def save(self, *args, **kwargs): if not self.key: self.key = ApiKey.generate_api_key() super().save(*args, **kwargs) def get_allowed_origins(self): if not self.allowed_origins: return [] return filter(bool, self.allowed_origins.split("\n")) def get_audit_log_data(self): return { "label": self.label, "key": self.key, "scopes": self.get_scopes(), "status": self.status, } def get_scopes(self): if self.scope_list: return self.scope_list return [k for k, v in self.scopes.items() if v] def has_scope(self, scope): return scope in self.get_scopes()
class Organization(Model): """ An organization represents a group of individuals which maintain ownership of projects. """ __core__ = True name = models.CharField(max_length=64) slug = models.SlugField(unique=True) status = BoundedPositiveIntegerField( choices=OrganizationStatus.as_choices(), # south will generate a default value of `'<OrganizationStatus.ACTIVE: 0>'` # if `.value` is omitted default=OrganizationStatus.ACTIVE.value, ) date_added = models.DateTimeField(default=timezone.now) members = models.ManyToManyField( settings.AUTH_USER_MODEL, through="sentry.OrganizationMember", related_name="org_memberships", through_fields=("organization", "user"), ) default_role = models.CharField( choices=roles.get_choices(), max_length=32, default=roles.get_default().id ) flags = BitField( flags=( ( "allow_joinleave", "Allow members to join and leave teams without requiring approval.", ), ( "enhanced_privacy", "Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.", ), ( "disable_shared_issues", "Disable sharing of limited details on issues to anonymous users.", ), ( "early_adopter", "Enable early adopter status, gaining access to features prior to public release.", ), ("require_2fa", "Require and enforce two-factor authentication for all members."), ( "disable_new_visibility_features", "Temporarily opt out of new visibility features and ui", ), ), default=1, ) objects = OrganizationManager(cache_fields=("pk", "slug")) class Meta: app_label = "sentry" db_table = "sentry_organization" __repr__ = sane_repr("owner_id", "name", "slug") @classmethod def get_default(cls): """ Return the organization used in single organization mode. """ if settings.SENTRY_ORGANIZATION is not None: return cls.objects.get(id=settings.SENTRY_ORGANIZATION) return cls.objects.filter(status=OrganizationStatus.ACTIVE)[0] def __unicode__(self): return u"%s (%s)" % (self.name, self.slug) def save(self, *args, **kwargs): if not self.slug: lock = locks.get("slug:organization", duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance(self, self.name, reserved=RESERVED_ORGANIZATION_SLUGS) super(Organization, self).save(*args, **kwargs) else: super(Organization, self).save(*args, **kwargs) def delete(self): if self.is_default: raise Exception("You cannot delete the the default organization.") return super(Organization, self).delete() @cached_property def is_default(self): if not settings.SENTRY_SINGLE_ORGANIZATION: return False return self == type(self).get_default() def has_access(self, user, access=None): queryset = self.member_set.filter(user=user) if access is not None: queryset = queryset.filter(type__lte=access) return queryset.exists() def get_audit_log_data(self): return { "id": self.id, "slug": self.slug, "name": self.name, "status": int(self.status), "flags": int(self.flags), "default_role": self.default_role, } def get_owners(self): from sentry.models import User return User.objects.filter( sentry_orgmember_set__role=roles.get_top_dog().id, sentry_orgmember_set__organization=self, is_active=True, ) def get_default_owner(self): if not hasattr(self, "_default_owner"): self._default_owner = self.get_owners()[0] return self._default_owner def has_single_owner(self): from sentry.models import OrganizationMember count = OrganizationMember.objects.filter( organization=self, role=roles.get_top_dog().id, user__isnull=False, user__is_active=True )[:2].count() return count == 1 def merge_to(from_org, to_org): from sentry.models import ( ApiKey, AuditLogEntry, AuthProvider, Commit, OrganizationAvatar, OrganizationIntegration, OrganizationMember, OrganizationMemberTeam, Project, Release, ReleaseCommit, ReleaseEnvironment, ReleaseFile, ReleaseHeadCommit, Repository, Team, Environment, ) for from_member in OrganizationMember.objects.filter( organization=from_org, user__isnull=False ): logger = logging.getLogger("sentry.merge") try: to_member = OrganizationMember.objects.get( organization=to_org, user=from_member.user ) except OrganizationMember.DoesNotExist: from_member.update(organization=to_org) to_member = from_member else: qs = OrganizationMemberTeam.objects.filter( organizationmember=from_member, is_active=True ).select_related() for omt in qs: OrganizationMemberTeam.objects.create_or_update( organizationmember=to_member, team=omt.team, defaults={"is_active": True} ) logger.info( "user.migrate", extra={ "instance_id": from_member.id, "new_member_id": to_member.id, "from_organization_id": from_org.id, "to_organization_id": to_org.id, }, ) for from_team in Team.objects.filter(organization=from_org): try: with transaction.atomic(): from_team.update(organization=to_org) except IntegrityError: slugify_instance(from_team, from_team.name, organization=to_org) from_team.update(organization=to_org, slug=from_team.slug) logger.info( "team.migrate", extra={ "instance_id": from_team.id, "new_slug": from_team.slug, "from_organization_id": from_org.id, "to_organization_id": to_org.id, }, ) for from_project in Project.objects.filter(organization=from_org): try: with transaction.atomic(): from_project.update(organization=to_org) except IntegrityError: slugify_instance( from_project, from_project.name, organization=to_org, reserved=RESERVED_PROJECT_SLUGS, ) from_project.update(organization=to_org, slug=from_project.slug) logger.info( "project.migrate", extra={ "instance_id": from_project.id, "new_slug": from_project.slug, "from_organization_id": from_org.id, "to_organization_id": to_org.id, }, ) # TODO(jess): update this when adding unique constraint # on version, organization for releases for from_release in Release.objects.filter(organization=from_org): try: to_release = Release.objects.get(version=from_release.version, organization=to_org) except Release.DoesNotExist: Release.objects.filter(id=from_release.id).update(organization=to_org) else: Release.merge(to_release, [from_release]) logger.info( "release.migrate", extra={ "instance_id": from_release.id, "from_organization_id": from_org.id, "to_organization_id": to_org.id, }, ) def do_update(queryset, params): model_name = queryset.model.__name__.lower() try: with transaction.atomic(): queryset.update(**params) except IntegrityError: for instance in queryset: try: with transaction.atomic(): instance.update(**params) except IntegrityError: logger.info( "{}.migrate-skipped".format(model_name), extra={ "from_organization_id": from_org.id, "to_organization_id": to_org.id, }, ) else: logger.info( "{}.migrate".format(model_name), extra={ "instance_id": instance.id, "from_organization_id": from_org.id, "to_organization_id": to_org.id, }, ) else: logger.info( "{}.migrate".format(model_name), extra={"from_organization_id": from_org.id, "to_organization_id": to_org.id}, ) INST_MODEL_LIST = ( AuthProvider, ApiKey, AuditLogEntry, OrganizationAvatar, OrganizationIntegration, ReleaseEnvironment, ReleaseFile, ) ATTR_MODEL_LIST = (Commit, ReleaseCommit, ReleaseHeadCommit, Repository, Environment) for model in INST_MODEL_LIST: queryset = model.objects.filter(organization=from_org) do_update(queryset, {"organization": to_org}) for model in ATTR_MODEL_LIST: queryset = model.objects.filter(organization_id=from_org.id) do_update(queryset, {"organization_id": to_org.id}) # TODO: Make these a mixin def update_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.set_value(self, *args, **kwargs) def get_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.get_value(self, *args, **kwargs) def delete_option(self, *args, **kwargs): from sentry.models import OrganizationOption return OrganizationOption.objects.unset_value(self, *args, **kwargs) def send_delete_confirmation(self, audit_log_entry, countdown): from sentry import options from sentry.utils.email import MessageBuilder owners = self.get_owners() context = { "organization": self, "audit_log_entry": audit_log_entry, "eta": timezone.now() + timedelta(seconds=countdown), "url": absolute_uri(reverse("sentry-restore-organization", args=[self.slug])), } MessageBuilder( subject="%sOrganization Queued for Deletion" % (options.get("mail.subject-prefix"),), template="sentry/emails/org_delete_confirm.txt", html_template="sentry/emails/org_delete_confirm.html", type="org.confirm_delete", context=context, ).send_async([o.email for o in owners]) def flag_has_changed(self, flag_name): "Returns ``True`` if ``flag`` has changed since initialization." return getattr(self.old_value("flags"), flag_name, None) != getattr(self.flags, flag_name) def handle_2fa_required(self, request): from sentry.models import ApiKey from sentry.tasks.auth import remove_2fa_non_compliant_members actor_id = request.user.id if request.user and request.user.is_authenticated() else None api_key_id = ( request.auth.id if hasattr(request, "auth") and isinstance(request.auth, ApiKey) else None ) ip_address = request.META["REMOTE_ADDR"] remove_2fa_non_compliant_members.delay( self.id, actor_id=actor_id, actor_key_id=api_key_id, ip_address=ip_address ) def get_url_viewname(self): return "sentry-organization-issue-list" def get_url(self): return reverse(self.get_url_viewname(), args=[self.slug])
class BitFieldTestModel(models.Model): flags = BitField( flags=('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3', ), default=3, db_column='another_name' )
class ApiKey(Model): organization = FlexibleForeignKey('sentry.Organization', related_name='key_set') label = models.CharField(max_length=64, blank=True, default='Default') key = models.CharField(max_length=32, unique=True) scopes = BitField(flags=( ('project:read', 'project:read'), ('project:write', 'project:write'), ('project:delete', 'project:delete'), ('team:read', 'team:read'), ('team:write', 'team:write'), ('team:delete', 'team:delete'), ('event:read', 'event:read'), ('event:write', 'event:write'), ('event:delete', 'event:delete'), ('org:read', 'org:read'), ('org:write', 'org:write'), ('org:delete', 'org:delete'), )) status = BoundedPositiveIntegerField(default=0, choices=( (ApiKeyStatus.ACTIVE, _('Active')), (ApiKeyStatus.INACTIVE, _('Inactive')), ), db_index=True) date_added = models.DateTimeField(default=timezone.now) allowed_origins = models.TextField(blank=True, null=True) objects = BaseManager(cache_fields=('key', )) class Meta: app_label = 'sentry' db_table = 'sentry_apikey' __repr__ = sane_repr('organization_id', 'key') def __unicode__(self): return six.text_type(self.key) @classmethod def generate_api_key(cls): return uuid4().hex @property def is_active(self): return self.status == ApiKeyStatus.ACTIVE def save(self, *args, **kwargs): if not self.key: self.key = ApiKey.generate_api_key() super(ApiKey, self).save(*args, **kwargs) def get_allowed_origins(self): return filter(bool, self.allowed_origins.split('\n')) def get_audit_log_data(self): return { 'label': self.label, 'key': self.key, 'roles': int(self.roles), 'status': self.status, }
class Notification(models.Model): """ ENG: A task executed on receiving a signal. RUS: Диспетчер сигналов. Задача выполняется при получении сигнала. """ MODES = { 0: ('email', _('By Email')), 1: ('push', _('By Push Notification')), } RECIPIENTS_EMPTY_CHOICES_VALUE = ('0', _("Nobody")) """ "1": "owner", "2": "moderate_person", "3": "responsible_person", "4": "private_person", "5": "auditory_persons", "6": "regional_persons" """ RECIPIENTS_ROLES_CHOICES = (RECIPIENTS_EMPTY_CHOICES_VALUE, ) SPLIT_CHARSET = ',' name = models.CharField(max_length=255, verbose_name=_("Name")) transition = MultiSelectField( verbose_name=_('Transition'), max_length=400, dinamic_choices_model_attr='get_transition_choices', blank=True) notify_to_roles = MultiSelectField( verbose_name=_('Notify to roles'), max_length=255, default='0', dinamic_choices_model_attr='get_notification_recipients_roles_choices') copy_to = models.ManyToManyField('CustomerProxy', blank=True, limit_choices_to={'is_staff': True}) template = models.ForeignKey(EmailTemplate, verbose_name=_("Template"), limit_choices_to=Q(language__isnull=True) | Q(language='')) mode = BitField(flags=MODES, verbose_name=_('Mode'), default=Bit(0).mask) active = models.BooleanField(verbose_name=_("Active"), default=True) class Meta: app_label = APP_LABEL verbose_name = _("Notification") verbose_name_plural = _("Notifications") ordering = ('transition', ) def __str__(self): return self.name @staticmethod def get_senders_objects(): """ RUS: Получение списка моделей для которых можно отправлять уведомления :return: [ список моделей, ... ] """ return NotificationMixin._notification_classes.values() @staticmethod def get_transition_name(object_model, source, target): """ RUS: Получение форматированного наименования состояния :param object_model: модель объекта :param source: наименование начального состояния :param target: наименование конечного состояния :return: форматированную строку модель:начальное состояние: конечное состояние """ return '{}:{}:{}'.format(object_model.__name__.lower(), source, target) @classmethod def get_notification_recipients_roles_choices(cls): """ RUS: Получение типов получателей доступных для уведомления для всех моделей :return: ((id: _("title")), ...) """ choices = list(cls.RECIPIENTS_ROLES_CHOICES) senders_objects = cls.get_senders_objects() for sender in senders_objects: for obj in sender.get_notification_recipients_roles_choices(): if obj not in choices: choices.append(obj) return choices @classmethod def get_transition_choices(cls): """ RUS: Получение доступных вариантов переходов состояний для всех моделей :return: [(ransition_choice_name, transition_choice_title),...] """ choices = {} for clazz in cls.get_senders_objects(): for transition in clazz.get_notification_transitions(): transition_choice_name = cls.get_transition_name( clazz, transition.source, transition.target) transition_choice_title = "{}: {} - {} ({} - {})".format( clazz._meta.verbose_name, clazz.get_transition_name(transition.source), clazz.get_transition_name(transition.target), transition.source, transition.target) choices[transition_choice_name] = transition_choice_title return sorted(choices.items(), key=lambda item: item[1]) @classmethod def get_avalible_recipients_roles_for_notifications(cls): """ RUS: Получение всех доступных ролей для состояниий :return: словарь {'модель:начальное состояние:конечное состояние': [ ид роли, ...]} """ roles_dict = {} senders_objects = cls.get_senders_objects() empty_value = cls.RECIPIENTS_EMPTY_CHOICES_VALUE[0] for sender in senders_objects: sender_name = sender.__name__.lower() for key, value in sender.get_avalible_recipients_roles_for_notifications( ).items(): if not empty_value in value: value.append(empty_value) roles_dict["%s:%s" % (sender_name, key)] = value return roles_dict @classmethod def send_notification(cls, object, source, target, **kwargs): """ RUS: Главная функция для подписки на сигналы Отправка уведомлений по подписанным событиям :param object: объект уведомления :param source: начальное состояние :param target: конечное состояние :param kwargs: """ transition_name = cls.get_transition_name(type(object), source, target) for n in Notification.objects.filter( transition__contains=transition_name, active=True): if n.mode.email: n.notify_by_email(object, source, target) if n.mode.push: n.notify_by_push(object, source, target) def get_notify_recipients_roles(self): """ RUS: Получение списка ролей персон для уведомления из модели :return: [Идентивикатор роли,...] """ return [ recipient_id for recipient_id in self.notify_to_roles if recipient_id != self.RECIPIENTS_ROLES_CHOICES[0][0] ] def notify_by_email(self, object, source, target, **kwargs): """ RUS: Отправка уведомлениий по email :param object: Объект уведомления Entity :param source - имя начального состояния :param target - имя конечного состояния recipients - список сосотящий из (email, пользователя, класс сериализации пользователя) example: [([email protected], customer_object, CustomerSerializer), ...] """ recipients = [ ] # - list of tuples (recipient_email, recipient_object, recipient_serialaizer_cls) if self.copy_to: recipients.extend( object.get_email_notification_recipients(self.copy_to.all())) recipients_roles = self.get_notify_recipients_roles() if recipients_roles: recipients.extend( object.get_email_notification_recipients_by_roles( recipients_roles)) if recipients: self.notify(recipients, object, source, target, 'email') def notify_by_push(self, object, source, target, **kwargs): """ RUS: Отправка push уведомлениий :param object: Объект уведомления Entity :param source - имя начального состояния :param target - имя конечного состояния recipients - список сосотящий из (id пользователя, пользователя, класс сериализации пользователя) example: [(key, customer_object, CustomerSerializer), ...] """ if push is not None: recipients = [ ] # - list of tuples (recipient_key, recipient_object, recipient_serialaizer_cls) if self.copy_to: recipients.extend( object.get_push_notification_recipients( self.copy_to.all())) recipients_roles = self.get_notify_recipients_roles() if recipients_roles: recipients.extend( object.get_push_notification_recipients_by_roles( recipients_roles)) if recipients: self.notify(recipients, object, source, target, 'push') def notify(self, recipients, object, source, target, mode='email', **kwargs): """ RUS: Подготовка и отправка сообщений по списку получателей :param recipients: список сосотящий из [(email или push_id, пользователь, класс сериализации пользователя),...] :param object: объект уведомления :param source - имя начального состояния :param target - имя конечного состояния :param mode: 'email' или 'push' """ # подготовка общего контекста stored_request = object.stored_request[0] if isinstance( object.stored_request, (tuple, list)) else object.stored_request emulated_request = EmulateHttpRequest(object.customer, stored_request) authenticators = [ auth() for auth in api_settings.DEFAULT_AUTHENTICATION_CLASSES ] serialaizer_cls = object.get_serialaizer_class() entity_serializer = serialaizer_cls( object, context={ 'request': Request(emulated_request, authenticators=authenticators) }) language = stored_request.get('language') translation.activate(language) try: template = self.template.translated_templates.get( language=language) except EmailTemplate.DoesNotExist: template = self.template attachments = {} for notiatt in self.notificationattachment_set.all(): attachments[notiatt.attachment. original_filename] = notiatt.attachment.file.file context = { 'data': entity_serializer.data, 'ABSOLUTE_BASE_URI': emulated_request.build_absolute_uri().rstrip('/'), 'render_language': language, 'transition': { 'source': { 'name': source, 'title': object.get_transition_name(source) }, 'target': { 'name': target, 'title': object.get_transition_name(target) }, } } # отправка уведомления пользователям if mode == 'email': for recipient in recipients: try: email_validator(recipient[0]) except ValidationError: pass else: # подготовка контекста получателя recipient_serialaizer_cls = recipient[2] context['recipient'] = recipient_serialaizer_cls( recipient[1]).data mail.send(recipient[0], template=template, context=context, attachments=attachments, render_on_delivery=True) elif mode == 'push' and push is not None: for recipient in recipients: # подготовка контекста получателя recipient_serialaizer_cls = recipient[2] context['recipient'] = recipient_serialaizer_cls( recipient[1]).data push.send(recipient[0], template=template, context=context, render_on_delivery=True)
class CareerEvent(models.Model): STATUS_CHOICES = ( ('NA', 'Needs Approval'), ('A', 'Approved'), ('D', 'Deleted'), ) person = models.ForeignKey(Person, related_name="career_events") unit = models.ForeignKey(Unit) slug = AutoSlugField(populate_from='slug_string', unique_with=('person', ), slugify=make_slug, null=False, editable=False) start_date = models.DateField(null=False, blank=False) end_date = models.DateField(null=True, blank=True) comments = models.TextField(blank=True) event_type = models.CharField(max_length=10, choices=EVENT_TYPE_CHOICES) config = JSONField(default={}) flags = BitField(flags=EVENT_FLAGS, default=0) status = models.CharField(max_length=2, choices=STATUS_CHOICES, blank=False, default='') import_key = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) objects = CareerEventManager() class Meta: ordering = ( '-start_date', '-end_date', 'event_type', ) unique_together = (("person", "slug"), ) def __unicode__(self): return u"%s from %s to %s" % (self.get_event_type_display(), self.start_date, self.end_date) def save(self, editor, call_from_handler=False, *args, **kwargs): # we're doing to so we can add an audit trail later. assert editor.__class__.__name__ == 'Person' assert call_from_handler, "must save through handler" return super(CareerEvent, self).save(*args, **kwargs) def get_absolute_url(self): return reverse("faculty_event_view", args=[self.person.userid, self.slug]) def get_status_change_url(self): return reverse("faculty_change_event_status", args=[self.person.userid, self.slug]) def get_change_url(self): return reverse("faculty_change_event", args=[self.person.userid, self.slug]) @property def slug_string(self): return u'{} {}'.format(self.start_date.year, self.get_event_type_display()) def handler_type_name(self): return self.get_handler().NAME @classmethod @cached(6 * 3600) def current_ranks(cls, person): """ Return a string representing the current rank(s) for this person """ salaries = CareerEvent.objects.filter( person=person, event_type='SALARY').effective_now() if not salaries: return 'unknown' ranks = set(s.get_handler().get_rank_display() for s in salaries) return ', '.join(ranks) def get_event_type_display(self): "Override to display nicely" return EVENT_TYPES[self.event_type].NAME def get_handler(self): if not hasattr(self, 'handler_cache'): self.handler_cache = EVENT_TYPES[self.event_type](self) return self.handler_cache def get_duration_within_range(self, start, end): """ Returns the number of days the event overlaps with a given date range """ if (self.start_date < end and (self.end_date == None or self.end_date > start)): s = max(start, self.start_date) if self.end_date: e = min(end, self.end_date) else: e = end delta = e - s return delta.days return 0 def filter_classes(self): """ return the class="..." value for this event on the summary page (for filtering) """ today = datetime.date.today() classes = [] #if self.start_date <= today and (self.end_date == None or self.end_date >= today): if self.end_date == None or self.end_date >= today: classes.append('current') if self.flags.affects_teaching: classes.append('teach') if self.flags.affects_salary: classes.append('salary') return ' '.join(classes) def memo_info(self): """ Context dictionary for building memo text """ # basic personal stuff gender = self.person.gender() title = self.person.get_title() if gender == "M": hisher = "his" heshe = 'he' elif gender == "F": hisher = "her" heshe = 'she' else: hisher = "his/her" heshe = 'he/she' # grab event type specific config data handler = self.get_handler() config_data = copy.deepcopy(self.config) for key in config_data: try: config_data[key] = unicode(handler.get_display(key)) except AttributeError: pass start = self.start_date.strftime('%B %d, %Y') end = self.end_date.strftime('%B %d, %Y') if self.end_date else '???' ls = { # if changing, also update EVENT_TAGS above! # For security reasons, all values must be strings (to avoid presenting dangerous methods in templates) 'title': title, 'his_her': hisher, 'His_Her': hisher.title(), 'he_she': heshe, 'He_She': heshe.title(), 'first_name': self.person.first_name, 'last_name': self.person.last_name, 'start_date': start, 'end_date': end, 'current_rank': CareerEvent.current_ranks(self.person) } ls = dict(ls.items() + config_data.items()) return ls
class CareerEvent(models.Model): STATUS_CHOICES = ( ('NA', 'Needs Approval'), ('A', 'Approved'), ('D', 'Deleted'), ) person = models.ForeignKey(Person, related_name="career_events", on_delete=models.PROTECT) unit = models.ForeignKey(Unit, on_delete=models.PROTECT) slug = AutoSlugField(populate_from='slug_string', unique_with=('person', ), slugify=make_slug, null=False, editable=False) start_date = models.DateField(null=False, blank=False) end_date = models.DateField(null=True, blank=True) comments = models.TextField(blank=True) event_type = models.CharField(max_length=10, choices=EVENT_TYPE_CHOICES) config = JSONField(default=dict) flags = BitField(flags=EVENT_FLAGS, default=0) status = models.CharField(max_length=2, choices=STATUS_CHOICES, blank=False, default='') import_key = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) objects = CareerQuerySet.as_manager() class Meta: ordering = ( '-start_date', '-end_date', 'event_type', ) unique_together = (("person", "slug"), ) def __str__(self): return "%s from %s to %s" % (self.get_event_type_display(), self.start_date, self.end_date) def save(self, editor, call_from_handler=False, *args, **kwargs): # we're doing to so we can add an audit trail later. assert editor.__class__.__name__ == 'Person' assert call_from_handler, "must save through handler" return super(CareerEvent, self).save(*args, **kwargs) def get_absolute_url(self): return reverse("faculty:view_event", args=[self.person.userid, self.slug]) def get_status_change_url(self): return reverse("faculty:change_event_status", args=[self.person.userid, self.slug]) def get_change_url(self): return reverse("faculty:change_event", args=[self.person.userid, self.slug]) @property def slug_string(self): return '{} {}'.format(self.start_date.year, self.get_event_type_display()) def handler_type_name(self): return self.get_handler().NAME @classmethod @cached(6 * 3600) def current_ranks(cls, person_id): """ Return a string representing the current rank(s) for this person """ salaries = CareerEvent.objects.filter( person__id=person_id, event_type='SALARY').effective_now() if not salaries: return 'unknown' ranks = set(s.get_handler().get_rank_display() for s in salaries) return ', '.join(ranks) @classmethod @cached(6 * 3600) def ranks_as_of_semester(cls, person_id, semester): """ Return a string representing the rank(s) for this person as of the beginning of a given semester. """ salaries = CareerEvent.objects.filter( person__id=person_id, event_type='SALARY').effective_date(semester.start) if not salaries: return 'unknown' ranks = set(s.get_handler().get_rank_display() for s in salaries) return ', '.join(ranks) @classmethod @cached(6 * 3600) def current_base_salary(cls, person): """ Return a string representing the current base salary for this person. If the person has more than one currently effective one, they get added together. """ salaries = CareerEvent.objects.filter( person=person, event_type='SALARY').effective_now() if not salaries: return 'unknown' # One could theoretically have more than one active base salary (for example, if one is a member of more than # one school and gets a salary from both). In that case, add them up. total = Decimal(0) for s in salaries: if 'base_salary' in s.config: total += Decimal(s.config.get('base_salary')) # format it nicely with commas, see http://stackoverflow.com/a/10742904/185884 return str('$' + "{:,}".format(total)) @classmethod @cached(6 * 3600) def current_market_diff(cls, person): """ Return a string representing the current market differential for this person. """ diffs = CareerEvent.objects.filter( person=person, event_type='STIPEND').effective_now() if not diffs: return 'unknown' # Retention, market differentials, research chair stipends, and other adjustments are stored in the same # stipend type event. We only care about market differentials. marketdiffs = [ d for d in diffs if 'source' in d.config and d.config.get('source') == 'MARKETDIFF' ] if marketdiffs: # Just like base salaries, we could theoretically have more than one active at a given time, we think. # Let's add them up in that case total = Decimal(0) for diff in marketdiffs: if 'amount' in diff.config: total += Decimal(diff.config.get('amount')) return str('$' + "{:,}".format(total)) else: return 'unknown' def get_event_type_display_(self): "Override to display nicely" return EVENT_TYPES[self.event_type].NAME def get_handler(self): if not hasattr(self, 'handler_cache'): self.handler_cache = EVENT_TYPES[self.event_type](self) return self.handler_cache def get_duration_within_range(self, start, end): """ Returns the number of days the event overlaps with a given date range """ if (self.start_date < end and (self.end_date == None or self.end_date > start)): s = max(start, self.start_date) if self.end_date: e = min(end, self.end_date) else: e = end delta = e - s return delta.days return 0 def filter_classes(self): """ return the class="..." value for this event on the summary page (for filtering) """ today = datetime.date.today() classes = [] #if self.start_date <= today and (self.end_date == None or self.end_date >= today): if self.end_date == None or self.end_date >= today: classes.append('current') if self.flags.affects_teaching: classes.append('teach') if self.flags.affects_salary: classes.append('salary') return ' '.join(classes) def memo_info(self): """ Context dictionary for building memo text """ # basic personal stuff gender = self.person.gender() title = self.person.get_title() if gender == "M": hisher = "his" heshe = 'he' himher = 'him' elif gender == "F": hisher = "her" heshe = 'she' himher = 'her' else: hisher = "his/her" heshe = 'he/she' himher = 'him/her' # grab event type specific config data handler = self.get_handler() config_data = copy.deepcopy(self.config) for key in config_data: try: config_data[key] = str(handler.get_display(key)) except AttributeError: pass start = self.start_date.strftime('%B %d, %Y') end = self.end_date.strftime('%B %d, %Y') if self.end_date else '???' ls = { # if changing, also update EVENT_TAGS above! # For security reasons, all values must be strings (to avoid presenting dangerous methods in templates) 'title' : title, 'his_her' : hisher, 'His_Her' : hisher.title(), 'he_she' : heshe, 'He_She' : heshe.title(), 'him_her' : himher, 'Him_Her' : himher.title(), 'first_name': self.person.first_name, 'last_name': self.person.last_name, 'start_date': start, 'end_date': end, 'current_rank': CareerEvent.current_ranks(self.person.id), 'unit': self.unit.name, 'current_base_salary': CareerEvent.current_base_salary(self.person), 'current_market_diff': CareerEvent.current_market_diff(self.person), } ls = dict(list(ls.items()) + list(config_data.items())) return ls def has_memos(self): return Memo.objects.filter(career_event=self, hidden=False).count() > 0 def has_attachments(self): return DocumentAttachment.objects.filter(career_event=self, hidden=False).count() > 0
class Game(models.Model): """Game model""" GAME_FLAGS = ( ("fully_libre", "Fully libre"), ("open_engine", "Open engine only"), ("free", "Free"), ("freetoplay", "Free-to-play"), ("pwyw", "Pay what you want"), ("demo", "Has a demo"), ("protected", "Installer modification is restricted"), ) # These model fields are editable by the user TRACKED_FIELDS = [ "name", "year", "platforms", "genres", "publisher", "developer", "website", "description", "title_logo" ] name = models.CharField(max_length=200) slug = models.SlugField(unique=True, null=True, blank=True) year = models.IntegerField(null=True, blank=True) platforms = models.ManyToManyField(Platform) genres = models.ManyToManyField(Genre) publisher = models.ForeignKey( Company, related_name="published_game", null=True, blank=True, on_delete=models.SET_NULL, ) developer = models.ForeignKey( Company, related_name="developed_game", null=True, blank=True, on_delete=models.SET_NULL, ) website = models.CharField(max_length=200, blank=True) icon = models.ImageField(upload_to="games/icons", blank=True) title_logo = models.ImageField(upload_to="games/banners", blank=True) description = models.TextField(blank=True) is_public = models.BooleanField("Published", default=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) steamid = models.PositiveIntegerField(null=True, blank=True) gogslug = models.CharField(max_length=200, blank=True) gogid = models.PositiveIntegerField(null=True, blank=True) humblestoreid = models.CharField(max_length=200, blank=True) flags = BitField(flags=GAME_FLAGS) popularity = models.IntegerField(default=0) # Indicates whether this data row is a changeset for another data row. # If so, this attribute is not NULL and the value is the ID of the # corresponding data row change_for = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE) objects = GameManager() # pylint: disable=W0232, R0903 class Meta(object): ordering = ["name"] permissions = (("can_publish_game", "Can publish game"), ) def __str__(self): if self.change_for is None: return self.name return "[Changes for] " + self.change_for.name @staticmethod def autocomplete_search_fields(): return ("name__icontains", ) @property def website_url(self): """Returns self.website guaranteed to be a valid URI""" if not self.website: return None # Fall back to http if no protocol specified (cannot assume that https will work) has_protocol = "://" in self.website return "http://" + self.website if not has_protocol else self.website @property def website_url_hr(self): """Returns a human readable website URL (stripped protocols and trailing slashes)""" if not self.website: return None return self.website.split("https:", 1)[-1].split("http:", 1)[-1].strip("/") @property def banner_url(self): if self.title_logo: return reverse("get_banner", kwargs={"slug": self.slug}) @property def icon_url(self): if self.icon: return reverse("get_icon", kwargs={"slug": self.slug}) @property def flag_labels(self): """Return labels of active flags, suitable for display""" # pylint: disable=E1133; self.flags *is* iterable return [ self.flags.get_label(flag[0]) for flag in self.flags if flag[1] ] def get_change_model(self): """Returns a dictionary which can be used as initial value in forms""" return { "name": self.name, "year": self.year, "platforms": [x.id for x in list(self.platforms.all())], "genres": [x.id for x in list(self.genres.all())], # The Select2 dropdowns want ids instead of complete models "publisher": self.publisher.id if self.publisher else None, "developer": self.developer.id if self.developer else None, "website": self.website, "description": self.description, "title_logo": self.title_logo, } def get_changes(self): """Returns a dictionary of the changes""" changes = [] # From the considered fields, only those who differ will be returned for entry in self.TRACKED_FIELDS: old_value = getattr(self.change_for, entry) new_value = getattr(self, entry) # M2M relations to string if entry in ["platforms", "genres"]: old_value = ", ".join("[{0}]".format(str(x)) for x in list(old_value.all())) new_value = ", ".join("[{0}]".format(str(x)) for x in list(new_value.all())) if old_value != new_value: changes.append((entry, old_value, new_value)) return changes def apply_changes(self, change_set): """Applies user-suggested changes to this model""" self.name = change_set.name self.year = change_set.year self.platforms.set(change_set.platforms.all()) self.genres.set(change_set.genres.all()) self.publisher = change_set.publisher self.developer = change_set.developer self.website = change_set.website self.description = change_set.description self.title_logo = change_set.title_logo def has_installer(self): return self.installers.exists() or self.has_auto_installers() def has_auto_installers(self): return self.platforms.filter(default_installer__isnull=False).exists() def get_absolute_url(self): """Return the absolute url for a game""" if self.change_for: slug = self.change_for.slug else: slug = self.slug return reverse("game_detail", kwargs={"slug": slug}) def set_logo_from_steam(self): if self.title_logo or not self.steamid: return self.title_logo = ContentFile(steam.get_capsule(self.steamid), "%s.jpg" % self.steamid) def set_logo_from_steam_api(self, img_url): """Sets the game banner from the Steam API URLs""" self.title_logo = ContentFile(steam.get_image(self.steamid, img_url), "%s.jpg" % self.steamid) def set_icon_from_steam_api(self, img_url): """Sets the game icon from the Steam API URLs""" self.icon = ContentFile(steam.get_image(self.steamid, img_url), "%s.jpg" % self.steamid) def set_logo_from_gog(self, gog_game): """Sets the game logo from the data retrieved from GOG""" if self.title_logo or not self.gogid: return self.title_logo = ContentFile(gog.get_logo(gog_game), "gog-%s.jpg" % self.gogid) def steam_support(self): """ Return the platform supported by Steam """ if not self.steamid: return False platforms = [p.slug for p in self.platforms.all()] if "linux" in platforms: return "linux" if "windows" in platforms: return "windows" return True def get_default_installers(self): auto_installers = [] for platform in self.platforms.all(): if platform.default_installer: installer = platform.default_installer installer["name"] = self.name installer["game_slug"] = self.slug installer["version"] = platform.name installer["slug"] = "-".join( (self.slug[:30], platform.slug[:20])) installer["platform"] = platform.slug installer["description"] = "" installer["published"] = True installer["auto"] = True auto_installers.append(installer) return auto_installers def check_for_submission(self): # Skip freshly created and unpublished objects if not self.pk or not self.is_public: return # Skip objects that were already published original = Game.objects.get(pk=self.pk) if original.is_public: return try: submission = GameSubmission.objects.get(game=self, accepted_at__isnull=True) except GameSubmission.DoesNotExist: pass else: submission.accept() def save(self, force_insert=False, force_update=False, using=None, update_fields=None): # Only create slug etc. if this is a game submission, no change submission if not self.change_for: if not self.slug: self.slug = slugify(self.name)[:50] if not self.slug: raise ValueError("Can't generate a slug for name %s" % self.name) self.set_logo_from_steam() self.check_for_submission() return super(Game, self).save( force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields, )
class AuthProvider(Model): __include_in_export__ = True organization = FlexibleForeignKey("sentry.Organization", unique=True) provider = models.CharField(max_length=128) config = EncryptedJsonField() date_added = models.DateTimeField(default=timezone.now) sync_time = BoundedPositiveIntegerField(null=True) last_sync = models.DateTimeField(null=True) default_role = BoundedPositiveIntegerField(default=50) default_global_access = models.BooleanField(default=True) # TODO(dcramer): ManyToMany has the same issue as ForeignKey and we need # to either write our own which works w/ BigAuto or switch this to use # through. default_teams = models.ManyToManyField("sentry.Team", blank=True) flags = BitField( flags=( ("allow_unlinked", "Grant access to members who have not linked SSO accounts."), ("scim_enabled", "Enable SCIM for member and team provisioning and syncing"), ), default=0, ) class Meta: app_label = "sentry" db_table = "sentry_authprovider" __repr__ = sane_repr("organization_id", "provider") def __str__(self): return self.provider def get_provider(self): from sentry.auth import manager return manager.get(self.provider, **self.config) @property def provider_name(self) -> str: return self.get_provider().name def get_scim_token(self): from sentry.models import SentryAppInstallationToken if self.flags.scim_enabled: return SentryAppInstallationToken.objects.get_token( self.organization, f"{self.provider}_scim") else: logger.warning( "SCIM disabled but tried to access token", extra={"organization_id": self.organization.id}, ) return None def get_scim_url(self): if self.flags.scim_enabled: url_prefix = options.get("system.url-prefix") # the SCIM protocol doesn't use trailing slashes in URLs return f"{url_prefix}/api/0/organizations/{self.organization.slug}/scim/v2" else: return None def enable_scim(self, user): from sentry.mediators.sentry_apps import InternalCreator from sentry.models import SentryAppInstallation, SentryAppInstallationForProvider if (not self.get_provider().can_use_scim(self.organization, user) or self.flags.scim_enabled is True): logger.warning( "SCIM already enabled", extra={"organization_id": self.organization.id}, ) return # check if we have a scim app already if SentryAppInstallationForProvider.objects.filter( organization=self.organization, provider="okta_scim").exists(): logger.warning( "SCIM installation already exists", extra={"organization_id": self.organization.id}, ) return data = { "name": "SCIM Internal Integration", "author": "Auto-generated by Sentry", "organization": self.organization, "overview": SCIM_INTERNAL_INTEGRATION_OVERVIEW, "user": user, "scopes": [ "member:read", "member:write", "member:admin", "team:write", "team:admin", ], } # create the internal integration and link it to the join table sentry_app = InternalCreator.run(**data) sentry_app_installation = SentryAppInstallation.objects.get( sentry_app=sentry_app) SentryAppInstallationForProvider.objects.create( sentry_app_installation=sentry_app_installation, organization=self.organization, provider=f"{self.provider}_scim", ) self.flags.scim_enabled = True def disable_scim(self, user): from sentry.mediators.sentry_apps import Destroyer from sentry.models import SentryAppInstallationForProvider if self.flags.scim_enabled: install = SentryAppInstallationForProvider.objects.get( organization=self.organization, provider=f"{self.provider}_scim") Destroyer.run( sentry_app=install.sentry_app_installation.sentry_app, user=user) self.flags.scim_enabled = False def get_audit_log_data(self): return {"provider": self.provider, "config": self.config}
class User(BaseModel, AbstractBaseUser): __core__ = True id = BoundedAutoField(primary_key=True) username = models.CharField(_("username"), max_length=128, unique=True) # this column is called first_name for legacy reasons, but it is the entire # display name name = models.CharField(_("name"), max_length=200, blank=True, db_column="first_name") email = models.EmailField(_("email address"), blank=True, max_length=75) is_staff = models.BooleanField( _("staff status"), default=False, help_text=_( "Designates whether the user can log into this admin site."), ) is_active = models.BooleanField( _("active"), default=True, help_text=_("Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts."), ) is_superuser = models.BooleanField( _("superuser status"), default=False, help_text= _("Designates that this user has all permissions without explicitly assigning them." ), ) is_managed = models.BooleanField( _("managed"), default=False, help_text=_("Designates whether this user should be treated as " "managed. Select this to disallow the user from " "modifying their account (username, password, etc)."), ) is_sentry_app = models.NullBooleanField( _("is sentry app"), null=True, default=None, help_text=_( "Designates whether this user is the entity used for Permissions" "on behalf of a Sentry App. Cannot login or use Sentry like a" "normal User would."), ) is_password_expired = models.BooleanField( _("password expired"), default=False, help_text=_("If set to true then the user needs to change the " "password on next sign in."), ) last_password_change = models.DateTimeField( _("date of last password change"), null=True, help_text=_("The date the password was changed last."), ) flags = BitField( flags=(("newsletter_consent_prompt", "Do we need to ask this user for newsletter consent?"), ), default=0, null=True, ) session_nonce = models.CharField(max_length=12, null=True) actor = FlexibleForeignKey("sentry.Actor", db_index=True, unique=True, null=True, on_delete=models.PROTECT) date_joined = models.DateTimeField(_("date joined"), default=timezone.now) last_active = models.DateTimeField(_("last active"), default=timezone.now, null=True) objects = UserManager(cache_fields=["pk"]) USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] class Meta: app_label = "sentry" db_table = "auth_user" verbose_name = _("user") verbose_name_plural = _("users") __repr__ = sane_repr("id") def delete(self): if self.username == "sentry": raise Exception( 'You cannot delete the "sentry" user as it is required by Sentry.' ) avatar = self.avatar.first() if avatar: avatar.delete() return super().delete() def save(self, *args, **kwargs): if not self.username: self.username = self.email return super().save(*args, **kwargs) def has_perm(self, perm_name): warnings.warn("User.has_perm is deprecated", DeprecationWarning) return self.is_superuser def has_module_perms(self, app_label): warnings.warn("User.has_module_perms is deprecated", DeprecationWarning) return self.is_superuser def get_unverified_emails(self): return self.emails.filter(is_verified=False) def get_verified_emails(self): return self.emails.filter(is_verified=True) def has_unverified_emails(self): return self.get_unverified_emails().exists() def get_label(self): return self.email or self.username or self.id def get_display_name(self): return self.name or self.email or self.username def get_full_name(self): return self.name def get_short_name(self): return self.username def get_salutation_name(self): name = self.name or self.username.split("@", 1)[0].split(".", 1)[0] first_name = name.split(" ", 1)[0] return first_name.capitalize() def get_avatar_type(self): avatar = self.avatar.first() if avatar: return avatar.get_avatar_type_display() return "letter_avatar" def send_confirm_email_singular(self, email, is_new_user=False): from sentry import options from sentry.utils.email import MessageBuilder if not email.hash_is_valid(): email.set_hash() email.save() context = { "user": self, "url": absolute_uri( reverse("sentry-account-confirm-email", args=[self.id, email.validation_hash])), "confirm_email": email.email, "is_new_user": is_new_user, } msg = MessageBuilder( subject="{}Confirm Email".format( options.get("mail.subject-prefix")), template="sentry/emails/confirm_email.txt", html_template="sentry/emails/confirm_email.html", type="user.confirm_email", context=context, ) msg.send_async([email.email]) def send_confirm_emails(self, is_new_user=False): email_list = self.get_unverified_emails() for email in email_list: self.send_confirm_email_singular(email, is_new_user) def merge_to(from_user, to_user): # TODO: we could discover relations automatically and make this useful from sentry import roles from sentry.models import ( Activity, AuditLogEntry, Authenticator, AuthIdentity, GroupAssignee, GroupBookmark, GroupSeen, GroupShare, GroupSubscription, Identity, OrganizationMember, OrganizationMemberTeam, UserAvatar, UserEmail, UserOption, ) audit_logger.info("user.merge", extra={ "from_user_id": from_user.id, "to_user_id": to_user.id }) for obj in OrganizationMember.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) # this will error if both users are members of obj.org except IntegrityError: pass # identify the highest priority membership # only applies if both users are members of obj.org # if roles are different, grants combined user the higher of the two to_member = OrganizationMember.objects.get( organization=obj.organization_id, user=to_user) if roles.get(obj.role).priority > roles.get( to_member.role).priority: to_member.update(role=obj.role) for team in obj.teams.all(): try: with transaction.atomic(): OrganizationMemberTeam.objects.create( organizationmember=to_member, team=team) # this will error if both users are on the same team in obj.org, # in which case, no need to update anything except IntegrityError: pass model_list = ( Authenticator, GroupAssignee, GroupBookmark, GroupSeen, GroupShare, GroupSubscription, Identity, UserAvatar, UserEmail, UserOption, ) for model in model_list: for obj in model.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) except IntegrityError: pass Activity.objects.filter(user=from_user).update(user=to_user) # users can be either the subject or the object of actions which get logged AuditLogEntry.objects.filter(actor=from_user).update(actor=to_user) AuditLogEntry.objects.filter(target_user=from_user).update( target_user=to_user) # remove any SSO identities that exist on from_user that might conflict # with to_user's existing identities (only applies if both users have # SSO identities in the same org), then pass the rest on to to_user AuthIdentity.objects.filter( user=from_user, auth_provider__organization__in=AuthIdentity.objects.filter( user=to_user).values("auth_provider__organization"), ).delete() AuthIdentity.objects.filter(user=from_user).update(user=to_user) def set_password(self, raw_password): super().set_password(raw_password) self.last_password_change = timezone.now() self.is_password_expired = False def refresh_session_nonce(self, request=None): from django.utils.crypto import get_random_string self.session_nonce = get_random_string(12) if request is not None: request.session["_nonce"] = self.session_nonce def get_orgs(self): from sentry.models import Organization, OrganizationMember, OrganizationStatus return Organization.objects.filter( status=OrganizationStatus.VISIBLE, id__in=OrganizationMember.objects.filter( user=self).values("organization"), ) def get_projects(self): from sentry.models import OrganizationMemberTeam, Project, ProjectStatus, ProjectTeam return Project.objects.filter( status=ProjectStatus.VISIBLE, id__in=ProjectTeam.objects.filter( team_id__in=OrganizationMemberTeam.objects.filter( organizationmember__user=self).values_list( "team_id", flat=True)).values_list("project_id", flat=True), ) def get_orgs_require_2fa(self): from sentry.models import Organization, OrganizationStatus return Organization.objects.filter( flags=models.F("flags").bitor(Organization.flags.require_2fa), status=OrganizationStatus.VISIBLE, member_set__user=self, ) def clear_lost_passwords(self): LostPasswordHash.objects.filter(user=self).delete()
def test_insert_neg(self): bitfield = BitField(3, 5) packed = bitfield.insert(-1, 0) self.assertEqual(packed, 0b000_111_000) unpacked = bitfield.extract_signed(packed) self.assertEqual(unpacked, -1)