class UserProfile(UserProfilePrivacyModel): REFERRAL_SOURCE_CHOICES = ( ('direct', 'Mozillians'), ('contribute', 'Get Involved'), ) objects = UserProfileManager.from_queryset(UserProfileQuerySet)() user = models.OneToOneField(User) full_name = models.CharField(max_length=255, default='', blank=False, verbose_name=_lazy(u'Full Name')) full_name_local = models.CharField( max_length=255, blank=True, default='', verbose_name=_lazy(u'Name in local language')) is_vouched = models.BooleanField( default=False, help_text='You can edit vouched status by editing invidual vouches') can_vouch = models.BooleanField( default=False, help_text='You can edit can_vouch status by editing invidual vouches') last_updated = models.DateTimeField(auto_now=True) groups = models.ManyToManyField(Group, blank=True, related_name='members', through=GroupMembership) skills = models.ManyToManyField(Skill, blank=True, related_name='members') bio = models.TextField(verbose_name=_lazy(u'Bio'), default='', blank=True) photo = ImageField(default='', blank=True, upload_to=_calculate_photo_filename) ircname = models.CharField(max_length=63, verbose_name=_lazy(u'IRC Nickname'), default='', blank=True) # validated geo data (validated that it's valid geo data, not that the # mozillian is there :-) ) geo_country = models.ForeignKey('geo.Country', blank=True, null=True, on_delete=models.SET_NULL) geo_region = models.ForeignKey('geo.Region', blank=True, null=True, on_delete=models.SET_NULL) geo_city = models.ForeignKey('geo.City', blank=True, null=True, on_delete=models.SET_NULL) lat = models.FloatField(_lazy(u'Latitude'), blank=True, null=True) lng = models.FloatField(_lazy(u'Longitude'), blank=True, null=True) # django-cities-light fields city = models.ForeignKey('cities_light.City', blank=True, null=True, on_delete=models.SET_NULL) region = models.ForeignKey('cities_light.Region', blank=True, null=True, on_delete=models.SET_NULL) country = models.ForeignKey('cities_light.Country', blank=True, null=True, on_delete=models.SET_NULL) allows_community_sites = models.BooleanField( default=True, verbose_name=_lazy(u'Sites that can determine my vouched status'), choices=((True, _lazy(u'All Community Sites')), (False, _lazy(u'Only Mozilla Properties')))) allows_mozilla_sites = models.BooleanField( default=True, verbose_name=_lazy(u'Allow Mozilla sites to access my profile data?'), choices=((True, _lazy(u'Yes')), (False, _lazy(u'No')))) basket_token = models.CharField(max_length=1024, default='', blank=True) date_mozillian = models.DateField('When was involved with Mozilla', null=True, blank=True, default=None) timezone = models.CharField(max_length=100, blank=True, default='', choices=zip(common_timezones, common_timezones)) tshirt = models.IntegerField( _lazy(u'T-Shirt'), blank=True, null=True, default=None, choices=((1, _lazy(u'Fitted Small')), (2, _lazy(u'Fitted Medium')), (3, _lazy(u'Fitted Large')), (4, _lazy(u'Fitted X-Large')), (5, _lazy(u'Fitted XX-Large')), (6, _lazy(u'Fitted XXX-Large')), (7, _lazy(u'Straight-cut Small')), (8, _lazy(u'Straight-cut Medium')), (9, _lazy(u'Straight-cut Large')), (10, _lazy(u'Straight-cut X-Large')), (11, _lazy(u'Straight-cut XX-Large')), (12, _lazy(u'Straight-cut XXX-Large')))) title = models.CharField(_lazy(u'What do you do for Mozilla?'), max_length=70, blank=True, default='') story_link = models.URLField( _lazy(u'Link to your contribution story'), help_text=_lazy(u'If you have created something public that ' u'tells the story of how you came to be a ' u'Mozillian, specify that link here.'), max_length=1024, blank=True, default='') referral_source = models.CharField(max_length=32, choices=REFERRAL_SOURCE_CHOICES, default='direct') def __unicode__(self): """Return this user's name when their profile is called.""" return self.display_name def get_absolute_url(self): return reverse('phonebook:profile_view', args=[self.user.username]) class Meta: db_table = 'profile' ordering = ['full_name'] def __getattribute__(self, attrname): """Special privacy aware __getattribute__ method. This method returns the real value of the attribute of object, if the privacy_level of the attribute is at least as large as the _privacy_level attribute. Otherwise it returns a default privacy respecting value for the attribute, as defined in the privacy_fields dictionary. special_functions provides methods that privacy safe their respective properties, where the privacy modifications are more complex. """ _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) privacy_fields = UserProfile.privacy_fields() privacy_level = _getattr('_privacy_level') special_functions = { 'accounts': '_accounts', 'alternate_emails': '_alternate_emails', 'email': '_primary_email', 'is_public_indexable': '_is_public_indexable', 'languages': '_languages', 'vouches_made': '_vouches_made', 'vouches_received': '_vouches_received', 'vouched_by': '_vouched_by', 'websites': '_websites' } if attrname in special_functions: return _getattr(special_functions[attrname]) if not privacy_level or attrname not in privacy_fields: return _getattr(attrname) field_privacy = _getattr('privacy_%s' % attrname) if field_privacy < privacy_level: return privacy_fields.get(attrname) return _getattr(attrname) def _filter_accounts_privacy(self, accounts): if self._privacy_level: return accounts.filter(privacy__gte=self._privacy_level) return accounts @property def _accounts(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) excluded_types = [ ExternalAccount.TYPE_WEBSITE, ExternalAccount.TYPE_EMAIL ] accounts = _getattr('externalaccount_set').exclude( type__in=excluded_types) return self._filter_accounts_privacy(accounts) @property def _alternate_emails(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) accounts = _getattr('externalaccount_set').filter( type=ExternalAccount.TYPE_EMAIL) return self._filter_accounts_privacy(accounts) @property def _is_public_indexable(self): for field in PUBLIC_INDEXABLE_FIELDS: if getattr(self, field, None) and getattr( self, 'privacy_%s' % field, None) == PUBLIC: return True return False @property def _languages(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) if self._privacy_level > _getattr('privacy_languages'): return _getattr('language_set').none() return _getattr('language_set').all() @property def _primary_email(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) privacy_fields = UserProfile.privacy_fields() if self._privacy_level and _getattr( 'privacy_email') < self._privacy_level: email = privacy_fields['email'] return email return _getattr('user').email @property def _vouched_by(self): privacy_level = self._privacy_level voucher = (UserProfile.objects.filter( vouches_made__vouchee=self).order_by('vouches_made__date')) if voucher.exists(): voucher = voucher[0] if privacy_level: voucher.set_instance_privacy_level(privacy_level) for field in UserProfile.privacy_fields(): if getattr(voucher, 'privacy_%s' % field) >= privacy_level: return voucher return None return voucher return None def _vouches(self, type): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) vouch_ids = [] for vouch in _getattr(type).all(): vouch.vouchee.set_instance_privacy_level(self._privacy_level) for field in UserProfile.privacy_fields(): if getattr(vouch.vouchee, 'privacy_%s' % field, 0) >= self._privacy_level: vouch_ids.append(vouch.id) vouches = _getattr(type).filter(pk__in=vouch_ids) return vouches @property def _vouches_made(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) if self._privacy_level: return self._vouches('vouches_made') return _getattr('vouches_made') @property def _vouches_received(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) if self._privacy_level: return self._vouches('vouches_received') return _getattr('vouches_received') @property def _websites(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) accounts = _getattr('externalaccount_set').filter( type=ExternalAccount.TYPE_WEBSITE) return self._filter_accounts_privacy(accounts) @property def display_name(self): return self.full_name @property def privacy_level(self): """Return user privacy clearance.""" if (self.user.groups.filter(name='Managers').exists() or self.user.is_superuser): return PRIVILEGED if self.groups.filter(name='staff').exists(): return EMPLOYEES if self.is_vouched: return MOZILLIANS return PUBLIC @property def is_complete(self): """Tests if a user has all the information needed to move on past the original registration view. """ return self.display_name.strip() != '' @property def is_public(self): """Return True is any of the privacy protected fields is PUBLIC.""" # TODO needs update for field in type(self).privacy_fields(): if getattr(self, 'privacy_%s' % field, None) == PUBLIC: return True return False @property def is_manager(self): return self.user.is_superuser or self.user.groups.filter( name='Managers').exists() @property def date_vouched(self): """ Return the date of the first vouch, if available.""" vouches = self.vouches_received.all().order_by('date')[:1] if vouches: return vouches[0].date return None def set_instance_privacy_level(self, level): """Sets privacy level of instance.""" self._privacy_level = level def set_privacy_level(self, level, save=True): """Sets all privacy enabled fields to 'level'.""" for field in type(self).privacy_fields(): setattr(self, 'privacy_%s' % field, level) if save: self.save() def set_membership(self, model, membership_list): """Alters membership to Groups and Skills.""" if model is Group: m2mfield = self.groups alias_model = GroupAlias elif model is Skill: m2mfield = self.skills alias_model = SkillAlias # Remove any visible groups that weren't supplied in this list. if model is Group: (GroupMembership.objects.filter( userprofile=self, group__visible=True).exclude( group__name__in=membership_list).delete()) else: m2mfield.remove(*[ g for g in m2mfield.all() if g.name not in membership_list and g.is_visible ]) # Add/create the rest of the groups groups_to_add = [] for g in membership_list: if alias_model.objects.filter(name=g).exists(): group = alias_model.objects.get(name=g).alias else: group = model.objects.create(name=g) if group.is_visible: groups_to_add.append(group) if model is Group: for group in groups_to_add: group.add_member(self) else: m2mfield.add(*groups_to_add) def get_photo_thumbnail(self, geometry='160x160', **kwargs): if 'crop' not in kwargs: kwargs['crop'] = 'center' if self.photo: return get_thumbnail(self.photo, geometry, **kwargs) return get_thumbnail(settings.DEFAULT_AVATAR_PATH, geometry, **kwargs) def get_photo_url(self, geometry='160x160', **kwargs): """Return photo url. If privacy allows and no photo set, return gravatar link. If privacy allows and photo set return local photo link. If privacy doesn't allow return default local link. """ privacy_level = getattr(self, '_privacy_level', MOZILLIANS) if (not self.photo and self.privacy_photo >= privacy_level): return gravatar(self.user.email, size=geometry) return absolutify(self.get_photo_thumbnail(geometry, **kwargs).url) def is_vouchable(self, voucher): """Check whether self can receive a vouch from voucher.""" # If there's a voucher, they must be able to vouch. if voucher and not voucher.can_vouch: return False # Maximum VOUCH_COUNT_LIMIT vouches per account, no matter what. if self.vouches_received.all().count() >= settings.VOUCH_COUNT_LIMIT: return False # If you've already vouched this account, you cannot do it again vouch_query = self.vouches_received.filter(voucher=voucher) if voucher and vouch_query.exists(): return False return True def vouch(self, vouched_by, description='', autovouch=False): if not self.is_vouchable(vouched_by): return vouch = self.vouches_received.create(voucher=vouched_by, date=datetime.now(), description=description, autovouch=autovouch) self._email_now_vouched(vouched_by, description) return vouch def auto_vouch(self): """Auto vouch mozilla.com users.""" emails = [ acc.identifier for acc in ExternalAccount.objects.filter( user=self, type=ExternalAccount.TYPE_EMAIL) ] emails.append(self.user.email) email_exists = any([ email for email in emails if email.split('@')[1] in settings.AUTO_VOUCH_DOMAINS ]) if email_exists and not self.vouches_received.filter( description=settings.AUTO_VOUCH_REASON, autovouch=True).exists(): self.vouch(None, settings.AUTO_VOUCH_REASON, autovouch=True) def _email_now_vouched(self, vouched_by, description=''): """Email this user, letting them know they are now vouched.""" name = None voucher_profile_link = None vouchee_profile_link = utils.absolutify(self.get_absolute_url()) if vouched_by: name = vouched_by.full_name voucher_profile_link = utils.absolutify( vouched_by.get_absolute_url()) number_of_vouches = self.vouches_received.all().count() template = get_template( 'phonebook/emails/vouch_confirmation_email.txt') message = template.render({ 'voucher_name': name, 'voucher_profile_url': voucher_profile_link, 'vouchee_profile_url': vouchee_profile_link, 'vouch_description': description, 'functional_areas_url': utils.absolutify(reverse('groups:index_functional_areas')), 'groups_url': utils.absolutify(reverse('groups:index_groups')), 'first_vouch': number_of_vouches == 1, 'can_vouch_threshold': number_of_vouches == settings.CAN_VOUCH_THRESHOLD, }) subject = _(u'You have been vouched on Mozillians.org') filtered_message = message.replace('"', '"').replace(''', "'") send_mail(subject, filtered_message, settings.FROM_NOREPLY, [self.user.email]) def get_annotated_groups(self): """ Return a list of all the visible groups the user is a member of or pending membership. The groups pending membership will have a .pending attribute set to True, others will have it set False. """ groups = [] # Query this way so we only get the groups that the privacy controls allow the # current user to see. We have to force evaluation of this query first, otherwise # Django combines the whole thing into one query and loses the privacy control. groups_manager = self.groups # checks to avoid AttributeError exception b/c self.groups may returns # EmptyQuerySet instead of the default manager due to privacy controls if hasattr(groups_manager, 'visible'): user_group_ids = list(groups_manager.visible().values_list( 'id', flat=True)) else: user_group_ids = [] for membership in self.groupmembership_set.filter( group_id__in=user_group_ids): group = membership.group group.pending = (membership.status == GroupMembership.PENDING) group.pending_terms = ( membership.status == GroupMembership.PENDING_TERMS) groups.append(group) return groups def timezone_offset(self): """ Return minutes the user's timezone is offset from UTC. E.g. if user is 4 hours behind UTC, returns -240. If user has not set a timezone, returns None (not 0). """ if self.timezone: return offset_of_timezone(self.timezone) def save(self, *args, **kwargs): self._privacy_level = None autovouch = kwargs.pop('autovouch', True) super(UserProfile, self).save(*args, **kwargs) # Auto_vouch follows the first save, because you can't # create foreign keys without a database id. if autovouch: self.auto_vouch()
class UserProfile(UserProfilePrivacyModel): REFERRAL_SOURCE_CHOICES = ( ('direct', 'Mozillians'), ('contribute', 'Get Involved'), ) objects = UserProfileManager.from_queryset(UserProfileQuerySet)() user = models.OneToOneField(User) full_name = models.CharField(max_length=255, default='', blank=False, verbose_name=_lazy(u'Full Name')) full_name_local = models.CharField( max_length=255, blank=True, default='', verbose_name=_lazy(u'Name in local language')) is_vouched = models.BooleanField( default=False, help_text='You can edit vouched status by editing invidual vouches') can_vouch = models.BooleanField( default=False, help_text='You can edit can_vouch status by editing invidual vouches') last_updated = models.DateTimeField(auto_now=True) groups = models.ManyToManyField(Group, blank=True, related_name='members', through=GroupMembership) skills = models.ManyToManyField(Skill, blank=True, related_name='members') bio = models.TextField(verbose_name=_lazy(u'Bio'), default='', blank=True) photo = ImageField(default='', blank=True, upload_to=_calculate_photo_filename) ircname = models.CharField(max_length=63, verbose_name=_lazy(u'IRC Nickname'), default='', blank=True) # validated geo data (validated that it's valid geo data, not that the # mozillian is there :-) ) geo_country = models.ForeignKey('geo.Country', blank=True, null=True, on_delete=models.SET_NULL) geo_region = models.ForeignKey('geo.Region', blank=True, null=True, on_delete=models.SET_NULL) geo_city = models.ForeignKey('geo.City', blank=True, null=True, on_delete=models.SET_NULL) lat = models.FloatField(_lazy(u'Latitude'), blank=True, null=True) lng = models.FloatField(_lazy(u'Longitude'), blank=True, null=True) # django-cities-light fields city = models.ForeignKey('cities_light.City', blank=True, null=True, on_delete=models.SET_NULL) region = models.ForeignKey('cities_light.Region', blank=True, null=True, on_delete=models.SET_NULL) country = models.ForeignKey('cities_light.Country', blank=True, null=True, on_delete=models.SET_NULL) basket_token = models.CharField(max_length=1024, default='', blank=True) date_mozillian = models.DateField('When was involved with Mozilla', null=True, blank=True, default=None) timezone = models.CharField(max_length=100, blank=True, default='', choices=zip(common_timezones, common_timezones)) tshirt = models.IntegerField( _lazy(u'T-Shirt'), blank=True, null=True, default=None, choices=((1, _lazy(u'Fitted Small')), (2, _lazy(u'Fitted Medium')), (3, _lazy(u'Fitted Large')), (4, _lazy(u'Fitted X-Large')), (5, _lazy(u'Fitted XX-Large')), (6, _lazy(u'Fitted XXX-Large')), (7, _lazy(u'Straight-cut Small')), (8, _lazy(u'Straight-cut Medium')), (9, _lazy(u'Straight-cut Large')), (10, _lazy(u'Straight-cut X-Large')), (11, _lazy(u'Straight-cut XX-Large')), (12, _lazy(u'Straight-cut XXX-Large')))) title = models.CharField(_lazy(u'What do you do for Mozilla?'), max_length=70, blank=True, default='') story_link = models.URLField( _lazy(u'Link to your contribution story'), help_text=_lazy(u'If you have created something public that ' u'tells the story of how you came to be a ' u'Mozillian, specify that link here.'), max_length=1024, blank=True, default='') referral_source = models.CharField(max_length=32, choices=REFERRAL_SOURCE_CHOICES, default='direct') def __unicode__(self): """Return this user's name when their profile is called.""" return self.display_name def get_absolute_url(self): return reverse('phonebook:profile_view', args=[self.user.username]) class Meta: db_table = 'profile' ordering = ['full_name'] def __getattribute__(self, attrname): """Special privacy aware __getattribute__ method. This method returns the real value of the attribute of object, if the privacy_level of the attribute is at least as large as the _privacy_level attribute. Otherwise it returns a default privacy respecting value for the attribute, as defined in the privacy_fields dictionary. special_functions provides methods that privacy safe their respective properties, where the privacy modifications are more complex. """ _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) privacy_fields = UserProfile.privacy_fields() privacy_level = _getattr('_privacy_level') special_functions = { 'accounts': '_accounts', 'alternate_emails': '_alternate_emails', 'email': '_primary_email', 'is_public_indexable': '_is_public_indexable', 'languages': '_languages', 'vouches_made': '_vouches_made', 'vouches_received': '_vouches_received', 'vouched_by': '_vouched_by', 'websites': '_websites', 'identity_profiles': '_identity_profiles' } if attrname in special_functions: return _getattr(special_functions[attrname]) if not privacy_level or attrname not in privacy_fields: return _getattr(attrname) field_privacy = _getattr('privacy_%s' % attrname) if field_privacy < privacy_level: return privacy_fields.get(attrname) return _getattr(attrname) def _filter_accounts_privacy(self, accounts): if self._privacy_level: return accounts.filter(privacy__gte=self._privacy_level) return accounts @property def _accounts(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) excluded_types = [ ExternalAccount.TYPE_WEBSITE, ExternalAccount.TYPE_EMAIL ] accounts = _getattr('externalaccount_set').exclude( type__in=excluded_types) return self._filter_accounts_privacy(accounts) @property def _alternate_emails(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) accounts = _getattr('externalaccount_set').filter( type=ExternalAccount.TYPE_EMAIL) return self._filter_accounts_privacy(accounts) @property def _api_alternate_emails(self): """ Helper private property that creates a compatibility layer for API results in alternate emails. Combines both IdpProfile and ExternalAccount objects. In conflicts/duplicates it returns the minimum privacy level defined. """ legacy_emails_qs = self._alternate_emails idp_qs = self._identity_profiles e_exclude = [ e.id for e in legacy_emails_qs if idp_qs.filter( email=e.identifier, privacy__gte=e.privacy).exists() ] legacy_emails_qs = legacy_emails_qs.exclude(id__in=e_exclude) idp_exclude = [ i.id for i in idp_qs if legacy_emails_qs.filter(identifier=i.email, privacy__gte=i.privacy).exists() ] idp_qs = idp_qs.exclude(id__in=idp_exclude) return chain(legacy_emails_qs, idp_qs) @property def _identity_profiles(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) accounts = _getattr('idp_profiles').all() return self._filter_accounts_privacy(accounts) @property def _is_public_indexable(self): for field in PUBLIC_INDEXABLE_FIELDS: if getattr(self, field, None) and getattr( self, 'privacy_%s' % field, None) == PUBLIC: return True return False @property def _languages(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) if self._privacy_level > _getattr('privacy_languages'): return _getattr('language_set').none() return _getattr('language_set').all() @property def _primary_email(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) privacy_fields = UserProfile.privacy_fields() if self._privacy_level: # Try IDP contact first if self.idp_profiles.exists(): contact_ids = self.identity_profiles.filter( primary_contact_identity=True) if contact_ids.exists(): return contact_ids[0].email return '' # Fallback to user.email if _getattr('privacy_email') < self._privacy_level: return privacy_fields['email'] # In case we don't have a privacy aware attribute access if self.idp_profiles.filter(primary_contact_identity=True).exists(): return self.idp_profiles.filter( primary_contact_identity=True)[0].email return _getattr('user').email @property def _vouched_by(self): privacy_level = self._privacy_level voucher = (UserProfile.objects.filter( vouches_made__vouchee=self).order_by('vouches_made__date')) if voucher.exists(): voucher = voucher[0] if privacy_level: voucher.set_instance_privacy_level(privacy_level) for field in UserProfile.privacy_fields(): if getattr(voucher, 'privacy_%s' % field) >= privacy_level: return voucher return None return voucher return None def _vouches(self, type): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) vouch_ids = [] for vouch in _getattr(type).all(): vouch.vouchee.set_instance_privacy_level(self._privacy_level) for field in UserProfile.privacy_fields(): if getattr(vouch.vouchee, 'privacy_%s' % field, 0) >= self._privacy_level: vouch_ids.append(vouch.id) vouches = _getattr(type).filter(pk__in=vouch_ids) return vouches @property def _vouches_made(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) if self._privacy_level: return self._vouches('vouches_made') return _getattr('vouches_made') @property def _vouches_received(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) if self._privacy_level: return self._vouches('vouches_received') return _getattr('vouches_received') @property def _websites(self): _getattr = (lambda x: super(UserProfile, self).__getattribute__(x)) accounts = _getattr('externalaccount_set').filter( type=ExternalAccount.TYPE_WEBSITE) return self._filter_accounts_privacy(accounts) @property def display_name(self): return self.full_name @property def privacy_level(self): """Return user privacy clearance.""" if (self.user.groups.filter(name='Managers').exists() or self.user.is_superuser): return PRIVATE if self.groups.filter(name='staff').exists(): return EMPLOYEES if self.is_vouched: return MOZILLIANS return PUBLIC @property def is_complete(self): """Tests if a user has all the information needed to move on past the original registration view. """ return self.display_name.strip() != '' @property def is_public(self): """Return True is any of the privacy protected fields is PUBLIC.""" # TODO needs update for field in type(self).privacy_fields(): if getattr(self, 'privacy_%s' % field, None) == PUBLIC: return True return False @property def is_manager(self): return self.user.is_superuser or self.user.groups.filter( name='Managers').exists() @property def is_nda(self): query = { 'userprofile__pk': self.pk, 'group__name': settings.NDA_GROUP, 'status': GroupMembership.MEMBER } return GroupMembership.objects.filter( **query).exists() or self.user.is_superuser @property def date_vouched(self): """ Return the date of the first vouch, if available.""" vouches = self.vouches_received.all().order_by('date')[:1] if vouches: return vouches[0].date return None @property def can_create_access_groups(self): """Check if a user can provision access groups. An access group is provisioned if a user holds an email in the AUTO_VOUCH_DOMAINS and has an LDAP IdpProfile or the user has a superuser account. """ emails = set([ idp.email for idp in IdpProfile.objects.filter(profile=self, type=IdpProfile.PROVIDER_LDAP) if idp.email.split('@')[1] in settings.AUTO_VOUCH_DOMAINS ]) if self.user.is_superuser or emails: return True return False def can_join_access_groups(self): """Check if a user can join access groups. A user can join an access group only if has an MFA account and belongs to the NDA group or is an employee. """ if self.can_create_access_groups or self.is_nda: return True return False def set_instance_privacy_level(self, level): """Sets privacy level of instance.""" self._privacy_level = level def set_privacy_level(self, level, save=True): """Sets all privacy enabled fields to 'level'.""" for field in type(self).privacy_fields(): setattr(self, 'privacy_%s' % field, level) if save: self.save() def set_membership(self, model, membership_list): """Alters membership to Groups and Skills.""" if model is Group: m2mfield = self.groups alias_model = GroupAlias elif model is Skill: m2mfield = self.skills alias_model = SkillAlias # Remove any visible groups that weren't supplied in this list. if model is Group: (GroupMembership.objects.filter( userprofile=self, group__visible=True).exclude( group__name__in=membership_list).delete()) else: m2mfield.remove(*[ g for g in m2mfield.all() if g.name not in membership_list and g.is_visible ]) # Add/create the rest of the groups groups_to_add = [] for g in membership_list: if alias_model.objects.filter(name=g).exists(): group = alias_model.objects.get(name=g).alias else: group = model.objects.create(name=g) if group.is_visible: groups_to_add.append(group) if model is Group: for group in groups_to_add: group.add_member(self) else: m2mfield.add(*groups_to_add) def get_photo_thumbnail(self, geometry='160x160', **kwargs): if 'crop' not in kwargs: kwargs['crop'] = 'center' if self.photo and default_storage.exists(self.photo): # Workaround for legacy images in RGBA model try: image_obj = Image.open(self.photo) except IOError: return get_thumbnail(settings.DEFAULT_AVATAR_PATH, geometry, **kwargs) if image_obj.mode == 'RGBA': new_fh = default_storage.open(self.photo.name, 'w') converted_image_obj = image_obj.convert('RGB') converted_image_obj.save(new_fh, 'JPEG') new_fh.close() return get_thumbnail(self.photo, geometry, **kwargs) return get_thumbnail(settings.DEFAULT_AVATAR_PATH, geometry, **kwargs) def get_photo_url(self, geometry='160x160', **kwargs): """Return photo url. If privacy allows and no photo set, return gravatar link. If privacy allows and photo set return local photo link. If privacy doesn't allow return default local link. """ privacy_level = getattr(self, '_privacy_level', MOZILLIANS) if (not self.photo and self.privacy_photo >= privacy_level): return gravatar(self.email, size=geometry) photo_url = self.get_photo_thumbnail(geometry, **kwargs).url if photo_url.startswith('https://') or photo_url.startswith('http://'): return photo_url return absolutify(photo_url) def is_vouchable(self, voucher): """Check whether self can receive a vouch from voucher.""" # If there's a voucher, they must be able to vouch. if voucher and not voucher.can_vouch: return False # Maximum VOUCH_COUNT_LIMIT vouches per account, no matter what. if self.vouches_received.all().count() >= settings.VOUCH_COUNT_LIMIT: return False # If you've already vouched this account, you cannot do it again vouch_query = self.vouches_received.filter(voucher=voucher) if voucher and vouch_query.exists(): return False return True def vouch(self, vouched_by, description='', autovouch=False): if not self.is_vouchable(vouched_by): return vouch = self.vouches_received.create(voucher=vouched_by, date=datetime.now(), description=description, autovouch=autovouch) self._email_now_vouched(vouched_by, description) return vouch def auto_vouch(self): """Auto vouch mozilla.com users.""" emails = [ acc.identifier for acc in ExternalAccount.objects.filter( user=self, type=ExternalAccount.TYPE_EMAIL) ] emails.append(self.email) email_exists = any([ email for email in emails if email.split('@')[1] in settings.AUTO_VOUCH_DOMAINS ]) if email_exists and not self.vouches_received.filter( description=settings.AUTO_VOUCH_REASON, autovouch=True).exists(): self.vouch(None, settings.AUTO_VOUCH_REASON, autovouch=True) def _email_now_vouched(self, vouched_by, description=''): """Email this user, letting them know they are now vouched.""" name = None voucher_profile_link = None vouchee_profile_link = utils.absolutify(self.get_absolute_url()) if vouched_by: name = vouched_by.full_name voucher_profile_link = utils.absolutify( vouched_by.get_absolute_url()) number_of_vouches = self.vouches_received.all().count() template = get_template( 'phonebook/emails/vouch_confirmation_email.txt') message = template.render({ 'voucher_name': name, 'voucher_profile_url': voucher_profile_link, 'vouchee_profile_url': vouchee_profile_link, 'vouch_description': description, 'functional_areas_url': utils.absolutify(reverse('groups:index_functional_areas')), 'groups_url': utils.absolutify(reverse('groups:index_groups')), 'first_vouch': number_of_vouches == 1, 'can_vouch_threshold': number_of_vouches == settings.CAN_VOUCH_THRESHOLD, }) subject = _(u'You have been vouched on Mozillians.org') filtered_message = message.replace('"', '"').replace(''', "'") send_mail(subject, filtered_message, settings.FROM_NOREPLY, [self.email]) def _get_annotated_groups(self): # Query this way so we only get the groups that the privacy controls allow the # current user to see. We have to force evaluation of this query first, otherwise # Django combines the whole thing into one query and loses the privacy control. groups_manager = self.groups # checks to avoid AttributeError exception b/c self.groups may returns # EmptyQuerySet instead of the default manager due to privacy controls user_group_ids = [] if hasattr(groups_manager, 'visible'): user_group_ids = groups_manager.visible().values_list('id', flat=True) return self.groupmembership_set.filter(group__id__in=user_group_ids) def get_annotated_tags(self): """ Return a list of all the visible tags the user is a member of or pending membership. The groups pending membership will have a .pending attribute set to True, others will have it set False. """ tags = self._get_annotated_groups().filter( group__is_access_group=False) annotated_tags = [] for membership in tags: tag = membership.group tag.pending = (membership.status == GroupMembership.PENDING) tag.pending_terms = ( membership.status == GroupMembership.PENDING_TERMS) annotated_tags.append(tag) return annotated_tags def get_annotated_access_groups(self): """ Return a list of all the visible access groups the user is a member of or pending membership. The groups pending membership will have a .pending attribute set to True, others will have it set False. There is also an inviter attribute which displays the inviter of the user in the group. """ access_groups = self._get_annotated_groups().filter( group__is_access_group=True) annotated_access_groups = [] for membership in access_groups: group = membership.group group.pending = (membership.status == GroupMembership.PENDING) group.pending_terms = ( membership.status == GroupMembership.PENDING_TERMS) try: invite = Invite.objects.get(group=membership.group, redeemer=self) except Invite.DoesNotExist: invite = None if invite: group.inviter = invite.inviter annotated_access_groups.append(group) return annotated_access_groups def get_cis_emails(self): """Prepares the entry for emails in the CIS format.""" idp_profiles = self.idp_profiles.all() primary_idp = idp_profiles.filter(primary=True) emails = [] primary_email = { 'value': self.email, 'verified': True, 'primary': True, 'name': 'mozillians-primary-{0}'.format(self.pk) } # We have an IdpProfile marked as primary (login identity) # If there is not an idp profile, the self.email is the one that is used to login if primary_idp.exists(): primary_email['value'] = primary_idp[0].email primary_email['name'] = primary_idp[0].get_type_display() emails.append(primary_email) # Non primary identity profiles for idp in self.idp_profiles.filter(primary=False): entry = { 'value': idp.email, 'verified': True, 'primary': False, 'name': '{0}'.format(idp.get_type_display()) } emails.append(entry) return emails def get_cis_uris(self): """Prepares the entry for URIs in the CIS format.""" accounts = [] for account in self.externalaccount_set.exclude( type=ExternalAccount.TYPE_EMAIL): value = account.get_identifier_url() account_type = ExternalAccount.ACCOUNT_TYPES[account.type] if value: entry = { 'value': value, 'primary': False, 'verified': False, 'name': 'mozillians-{}-{}'.format(account_type['name'], account.pk) } accounts.append(entry) return accounts def get_cis_groups(self, idp): """Prepares the entry for profile groups in the CIS format.""" # Update strategy: send groups for higher MFA idp # Wipe groups from the rest idps = list(self.idp_profiles.all().values_list('type', flat=True)) # if the current idp does not match # the greatest number in the list, wipe the groups if not idps or idp.type != max(idps) or not idp.is_mfa(): return [] memberships = GroupMembership.objects.filter( userprofile=self, status=GroupMembership.MEMBER, group__is_access_group=True) groups = ['mozilliansorg_{}'.format(m.group.url) for m in memberships] return groups def get_cis_tags(self): """Prepares the entry for profile tags in the CIS format.""" memberships = GroupMembership.objects.filter( userprofile=self, status=GroupMembership.MEMBER).exclude(group__is_access_group=True) tags = [m.group.url for m in memberships] return tags def timezone_offset(self): """ Return minutes the user's timezone is offset from UTC. E.g. if user is 4 hours behind UTC, returns -240. If user has not set a timezone, returns None (not 0). """ if self.timezone: return offset_of_timezone(self.timezone) def save(self, *args, **kwargs): self._privacy_level = None autovouch = kwargs.pop('autovouch', True) super(UserProfile, self).save(*args, **kwargs) # Auto_vouch follows the first save, because you can't # create foreign keys without a database id. if self.is_complete: send_userprofile_to_cis.delay(self.pk) if autovouch: self.auto_vouch()