Example #1
0
class BadgeUser(BaseVersionedEntity, AbstractUser, cachemodel.CacheModel):
    """
    A full-featured user model that can be an Earner, Issuer, or Consumer of Open Badges
    """
    entity_class_name = 'BadgeUser'

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    badgrapp = models.ForeignKey('mainsite.BadgrApp',
                                 blank=True,
                                 null=True,
                                 default=None)

    marketing_opt_in = models.BooleanField(default=False)

    objects = BadgeUserManager()

    class Meta:
        verbose_name = _('badge user')
        verbose_name_plural = _('badge users')
        db_table = 'users'

    def __unicode__(self):
        return u"{} <{}>".format(self.get_full_name(), self.email)

    def get_full_name(self):
        return u"%s %s" % (self.first_name, self.last_name)

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.primary_email], **kwargs)

    def publish(self):
        super(BadgeUser, self).publish()
        self.publish_by('username')

    def delete(self, *args, **kwargs):
        super(BadgeUser, self).delete(*args, **kwargs)
        self.publish_delete('username')

    @cachemodel.cached_method(auto_publish=True)
    def cached_emails(self):
        return CachedEmailAddress.objects.filter(user=self)

    @cachemodel.cached_method(auto_publish=True)
    def cached_backpackcollections(self):
        return BackpackCollection.objects.filter(created_by=self)

    @property
    def email_items(self):
        return self.cached_emails()

    @email_items.setter
    def email_items(self, value):
        """
        Update this users EmailAddress from a list of BadgeUserEmailSerializerV2 data
        :param value: list(BadgeUserEmailSerializerV2)
        :return: None
        """
        if len(value) < 1:
            raise ValidationError("Must have at least 1 email")

        new_email_idx = {d['email']: d for d in value}

        primary_count = sum(1 if d.get('primary', False) else 0 for d in value)
        if primary_count != 1:
            raise ValidationError("Must have exactly 1 primary email")

        with transaction.atomic():
            # add or update existing items
            for email_data in value:
                primary = email_data.get('primary', False)
                emailaddress, created = CachedEmailAddress.cached.get_or_create(
                    email=email_data['email'],
                    defaults={
                        'user': self,
                        'primary': primary
                    })
                if created:
                    # new email address send a confirmation
                    emailaddress.send_confirmation()
                else:
                    if emailaddress.user_id == self.id:
                        # existing email address owned by user
                        emailaddress.primary = primary
                        emailaddress.save()
                    elif not emailaddress.verified:
                        # existing unverified email address, handover to this user
                        emailaddress.user = self
                        emailaddress.primary = primary
                        emailaddress.save()
                        emailaddress.send_confirmation()
                    else:
                        # existing email address used by someone else
                        raise ValidationError(
                            "Email '{}' may already be in use".format(
                                email_data.get('email')))

            # remove old items
            for emailaddress in self.email_items:
                if emailaddress.email not in new_email_idx:
                    emailaddress.delete()

    def cached_email_variants(self):
        return chain.from_iterable(email.cached_variants()
                                   for email in self.cached_emails())

    def can_add_variant(self, email):
        try:
            canonical_email = CachedEmailAddress.objects.get(email=email,
                                                             user=self,
                                                             verified=True)
        except CachedEmailAddress.DoesNotExist:
            return False

        if email != canonical_email.email \
                and email not in [e.email for e in canonical_email.cached_variants()] \
                and EmailAddressVariant(email=email, canonical_email=canonical_email).is_valid():
            return True
        return False

    @property
    def primary_email(self):
        primaries = filter(lambda e: e.primary, self.cached_emails())
        if len(primaries) > 0:
            return primaries[0].email
        return self.email

    @property
    def verified_emails(self):
        return filter(lambda e: e.verified, self.cached_emails())

    @property
    def verified(self):
        if self.is_superuser:
            return True

        if len(self.verified_emails) > 0:
            return True

        return False

    @property
    def all_recipient_identifiers(self):
        return [e.email for e in self.cached_emails() if e.verified
                ] + [e.email for e in self.cached_email_variants()]

    @cachemodel.cached_method(auto_publish=True)
    def cached_issuers(self):
        return Issuer.objects.filter(staff__id=self.id).distinct()

    @property
    def peers(self):
        """
        a BadgeUser is a Peer of another BadgeUser if they appear in an IssuerStaff together
        """
        return set(
            chain(*[[s.cached_user for s in i.cached_issuerstaff()]
                    for i in self.cached_issuers()]))

    def cached_badgeclasses(self):
        return chain.from_iterable(issuer.cached_badgeclasses()
                                   for issuer in self.cached_issuers())

    @cachemodel.cached_method(auto_publish=True)
    def cached_badgeinstances(self):
        return BadgeInstance.objects.filter(
            recipient_identifier__in=self.all_recipient_identifiers)

    @cachemodel.cached_method(auto_publish=True)
    def cached_externaltools(self):
        return [
            a.cached_externaltool
            for a in self.externaltooluseractivation_set.filter(is_active=True)
        ]

    @cachemodel.cached_method(auto_publish=True)
    def cached_token(self):
        user_token, created = \
                Token.objects.get_or_create(user=self)
        return user_token.key

    @cachemodel.cached_method(auto_publish=True)
    def cached_agreed_terms_version(self):
        try:
            return self.termsagreement_set.all().order_by('-terms_version')[0]
        except IndexError:
            pass
        return None

    @property
    def agreed_terms_version(self):
        v = self.cached_agreed_terms_version()
        if v is None:
            return 0
        return v.terms_version

    @agreed_terms_version.setter
    def agreed_terms_version(self, value):
        try:
            value = int(value)
        except ValueError as e:
            return

        if value > self.agreed_terms_version:
            if TermsVersion.active_objects.filter(version=value).exists():
                if not self.pk:
                    self.save()
                self.termsagreement_set.get_or_create(
                    terms_version=value, defaults=dict(agreed=True))

    def replace_token(self):
        Token.objects.filter(user=self).delete()
        # user_token, created = \
        #         Token.objects.get_or_create(user=self)
        self.save()
        return self.cached_token()

    def save(self, *args, **kwargs):
        if not self.username:
            # md5 hash the email and then encode as base64 to take up only 25 characters
            hashed = md5(self.email + ''.join(
                random.choice(string.lowercase)
                for i in range(64))).digest().encode(
                    'base64')[:
                              -1]  # strip last character because its a newline
            self.username = "******".format(hashed[:25])

        if getattr(settings, 'BADGEUSER_SKIP_LAST_LOGIN_TIME', True):
            # skip saving last_login to the database
            if 'update_fields' in kwargs and kwargs[
                    'update_fields'] is not None and 'last_login' in kwargs[
                        'update_fields']:
                kwargs['update_fields'].remove('last_login')
                if len(kwargs['update_fields']) < 1:
                    # nothing to do, abort so we dont call .publish()
                    return
        return super(BadgeUser, self).save(*args, **kwargs)
Example #2
0
class BadgeUser(BaseVersionedEntity, AbstractUser, cachemodel.CacheModel):
    """
    A full-featured user model that can be an Earner, Issuer, or Consumer of Open Badges
    """
    entity_class_name = 'BadgeUser'

    badgrapp = models.ForeignKey('mainsite.BadgrApp', on_delete=models.SET_NULL, blank=True, null=True, default=None)
    faculty = models.ManyToManyField('institution.Faculty', blank=True)
    institution = models.ForeignKey('institution.Institution', on_delete=models.SET_NULL, blank=True, null=True, default=None)
    is_staff = models.BooleanField(
        _('Backend-staff member'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )

    # canvas LTI id
    lti_id = models.CharField(unique=True, max_length=50, default=None, null=True, blank=True,
                              help_text='LTI user id, unique per user')
    marketing_opt_in = models.BooleanField(default=False)

    objects = BadgeUserManager()

    class Meta:
        verbose_name = _('badge user')
        verbose_name_plural = _('badge users')
        db_table = 'users'
        permissions=(('view_issuer_tab', 'User can view Issuer tab in front end'),
                     ('view_management_tab', 'User can view Management dashboard'),
                     ('has_faculty_scope', 'User has faculty scope'),
                     ('has_institution_scope', 'User has institution scope'),
                     ('ui_issuer_add', 'User can add issuer in front end'),
                     )

    def __unicode__(self):
        return "{} <{}>".format(self.get_full_name(), self.email)


    def within_scope(self, object):
        if object:
            if self.has_perm('badgeuser.has_institution_scope'):
                return object.institution == self.institution
            if self.has_perm('badgeuser.has_faculty_scope'):
                if object.faculty.__class__.__name__ == 'ManyRelatedManager':
                    return bool(set(object.faculty.all()).intersection(set(self.faculty.all())))
                else:
                    return object.faculty in self.faculty.all()
        return False

    def get_badgr_app(self):
        if self.badgrapp:
            return self.badgrapp
        else:
            return BadgrApp.objects.all().first()

    def get_full_name(self):
        return "%s %s" % (self.first_name, self.last_name)

    def gains_permission(self, permission_codename, model):
        content_type = ContentType.objects.get_for_model(model)
        permission = Permission.objects.get(codename=permission_codename, content_type=content_type)
        self.user_permissions.add(permission)
        # you still need to reload user from db to refresh permission cache if you want effect to be immediate

    def loses_permission(self, permission_codename, model):
        content_type = ContentType.objects.get_for_model(model)
        permission = Permission.objects.get(codename=permission_codename, content_type=content_type)
        self.user_permissions.remove(permission)
        # you still need to reload user from db to refresh permission cache if you want effect to be immediate

    def email_user(self, subject, message, from_email=None, attachments=None, **kwargs):
        """
        Sends an email to this User.
        """
        try:
            EmailBlacklist.objects.get(email=self.primary_email)
        except EmailBlacklist.DoesNotExist:
            # Allow sending, as this email is not blacklisted.
            if not attachments:
                send_mail(subject, message, from_email, [self.primary_email], **kwargs)
            else:
                from django.core.mail import EmailMessage
                email = EmailMessage(subject=subject,
                                     body=message,
                                     from_email=from_email,
                                     to=[self.primary_email],
                                     attachments=attachments)
                email.send()
        else:
            return
            # TODO: Report email non-delivery somewhere.

    def publish(self):
        super(BadgeUser, self).publish()
        self.publish_by('username')

    def delete(self, *args, **kwargs):
        super(BadgeUser, self).delete(*args, **kwargs)
        self.publish_delete('username')

#     @cachemodel.cached_method(auto_publish=True)
    # turned it off, because if user logs in for FIRST time, this caching will result in the user having no verified emails. 
    # This results in api calls responding with a 403 after the failure of the AuthenticatedWithVerifiedEmail permission check.
    # Which will logout the user automatically with the error: Token expired.
    def cached_emails(self):
        return CachedEmailAddress.objects.filter(user=self)

    @cachemodel.cached_method(auto_publish=True)
    def cached_backpackcollections(self):
        return BackpackCollection.objects.filter(created_by=self)

    @property
    def email_items(self):
        return self.cached_emails()

    @property
    def may_sign_assertions(self):
        return self.has_perm('signing.may_sign_assertions')

    @property
    def current_symmetric_key(self):
        return self.symmetrickey_set.get(current=True)

    @email_items.setter
    def email_items(self, value):
        """
        Update this users EmailAddress from a list of BadgeUserEmailSerializerV2 data
        :param value: list(BadgeUserEmailSerializerV2)
        :return: None
        """
        if len(value) < 1:
            raise ValidationError("Must have at least 1 email")

        new_email_idx = {d['email']: d for d in value}

        primary_count = sum(1 if d.get('primary', False) else 0 for d in value)
        if primary_count != 1:
            raise ValidationError("Must have exactly 1 primary email")

        with transaction.atomic():
            # add or update existing items
            for email_data in value:
                primary = email_data.get('primary', False)
                emailaddress, created = CachedEmailAddress.cached.get_or_create(
                    email=email_data['email'],
                    defaults={
                        'user': self,
                        'primary': primary
                    })
                if created:
                    # new email address send a confirmation
                    emailaddress.send_confirmation()
                else:
                    if emailaddress.user_id == self.id:
                        # existing email address owned by user
                        emailaddress.primary = primary
                        emailaddress.save()
                    elif not emailaddress.verified:
                        # existing unverified email address, handover to this user
                        emailaddress.user = self
                        emailaddress.primary = primary
                        emailaddress.save()
                        emailaddress.send_confirmation()
                    else:
                        # existing email address used by someone else
                        raise ValidationError("Email '{}' may already be in use".format(email_data.get('email')))

            # remove old items
            for emailaddress in self.email_items:
                if emailaddress.email not in new_email_idx:
                    emailaddress.delete()

    def cached_email_variants(self):
        return chain.from_iterable(email.cached_variants() for email in self.cached_emails())

    def can_add_variant(self, email):
        try:
            canonical_email = CachedEmailAddress.objects.get(email=email, user=self, verified=True)
        except CachedEmailAddress.DoesNotExist:
            return False

        if email != canonical_email.email \
                and email not in [e.email for e in canonical_email.cached_variants()] \
                and EmailAddressVariant(email=email, canonical_email=canonical_email).is_valid():
            return True
        return False

    @property
    def primary_email(self):
        primaries = [e for e in self.cached_emails() if e.primary]
        if len(primaries) > 0:
            return primaries[0].email
        return self.email

    @property
    def verified_emails(self):
        return [e for e in self.cached_emails() if e.verified]

    @property
    def verified(self):
        if self.is_superuser:
            return True

        if len(self.verified_emails) > 0:
            return True

        return False

    @property
    def all_recipient_identifiers(self):
        return [self.get_recipient_identifier()]
#         return [e.email for e in self.cached_emails() if e.verified] + [e.email for e in self.cached_email_variants()]

    @property
    def highest_group(self):
        groups = list(self.groups.filter(entity_rank__rank__gte=0))
        groups.sort(key=lambda x: x.entity_rank.rank)
        if groups:
            return groups[0]
        else:
            return None

    def get_recipient_identifier(self):
        from allauth.socialaccount.models import SocialAccount
        try:
            account = SocialAccount.objects.get(user=self.pk)
            return account.extra_data['sub']
        except SocialAccount.DoesNotExist:
            return None

    def get_social_account(self):
        from allauth.socialaccount.models import SocialAccount
        try:
            account = SocialAccount.objects.get(user=self.pk)
            return account
        except SocialAccount.DoesNotExist:
            return None

    def has_edu_id_social_account(self):
        social_account = self.get_social_account()
        return social_account.provider == 'edu_id' or social_account.provider == 'surfconext_ala'

    def has_surf_conext_social_account(self):
        social_account = self.get_social_account()
        return social_account.provider == 'surf_conext'

    def staff_memberships(self):
        """
        Returns all staff memberships
        """
        return Issuer.objects.filter(staff__id=self.id)

    def is_email_verified(self, email):
        if email in [e.email for e in self.verified_emails]:
            return True

        try:
            app_infos = ApplicationInfo.objects.filter(application__user=self)
            if any(app_info.trust_email_verification for app_info in app_infos):
                return True
        except ApplicationInfo.DoesNotExist:
            return False

        return False

    def get_assertions_ready_for_signing(self):
        assertion_timestamps = AssertionTimeStamp.objects.filter(signer=self).exclude(proof='')
        return [ts.badge_instance for ts in assertion_timestamps if ts.badge_instance.signature == None]

    @cachemodel.cached_method(auto_publish=True)
    def cached_issuers(self):
        '''
        Creates a list of issuers belonging to the user's scope
        '''
        queryset = Issuer.objects.filter(staff__id=self.id) # personal issuers
        if self.has_perm('badgeuser.has_institution_scope'):
            institution = self.institution
            if institution:
                queryset = queryset | Issuer.objects.filter(faculty__institution=institution) # add insitution issuers
        elif self.has_perm('badgeuser.has_faculty_scope'):
            faculties = self.faculty.all()
            if faculties:
                queryset = queryset | Issuer.objects.filter(faculty__in=self.faculty.all()) # add faculty issuers
        return queryset.distinct()

    @property
    def peers(self):
        """
        a BadgeUser is a Peer of another BadgeUser if they appear in an IssuerStaff together
        """
        return set(chain(*[[s.cached_user for s in i.cached_issuerstaff()] for i in self.cached_issuers()]))

    def cached_badgeclasses(self):
        return chain.from_iterable(issuer.cached_badgeclasses() for issuer in self.cached_issuers())

    @cachemodel.cached_method(auto_publish=True)
    def cached_badgeinstances(self):
#         return BadgeInstance.objects.filter(recipient_identifier__in=self.all_recipient_identifiers)
        return BadgeInstance.objects.filter(recipient_identifier=self.get_recipient_identifier())

    @cachemodel.cached_method(auto_publish=True)
    def cached_externaltools(self):
        return [a.cached_externaltool for a in self.externaltooluseractivation_set.filter(is_active=True)]

    @cachemodel.cached_method(auto_publish=True)
    def cached_token(self):
        user_token, created = \
                Token.objects.get_or_create(user=self)
        return user_token.key

    @cachemodel.cached_method(auto_publish=True)
    def cached_agreed_terms_version(self):
        try:
            return self.termsagreement_set.all().filter(valid=True).order_by('-terms_version')[0]
        except IndexError:
            pass
        return None

    @property
    def agreed_terms_version(self):
        v = self.cached_agreed_terms_version()
        if v is None:
            return 0
        return v.terms_version

    @agreed_terms_version.setter
    def agreed_terms_version(self, value):
        try:
            value = int(value)
        except ValueError as e:
            return

        if value > self.agreed_terms_version:
            if TermsVersion.active_objects.filter(version=value).exists():
                if not self.pk:
                    self.save()
                self.termsagreement_set.get_or_create(terms_version=value, defaults=dict(agreed=True))


    def replace_token(self):
        Token.objects.filter(user=self).delete()
        # user_token, created = \
        #         Token.objects.get_or_create(user=self)
        self.save()
        return self.cached_token()

    def may_enroll(self, badge_class):
        """
        Checks to see if user may enroll
            no enrollments: May enroll
            any not awarded assertions: May not enroll
            Any awarded and not revoked: May not enroll
            All revoked: May enroll 
        """
        social_account = self.get_social_account()
        if social_account.provider == 'edu_id' or social_account.provider == 'surfconext_ala':
            enrollments = StudentsEnrolled.objects.filter(user=social_account.user, badge_class_id=badge_class.pk)
            if not enrollments:
                return True # no enrollments
            else:
                for enrollment in enrollments:
                    if not bool(enrollment.badge_instance): # has never been awarded
                        return False
                    else: #has been awarded
                        if not enrollment.assertion_is_revoked():
                            return False
                return True # all have been awarded and revoked
        else: # no eduID
            return False

    def save(self, *args, **kwargs):
        if not self.username:
            # md5 hash the email and then encode as base64 to take up only 25 characters
            hashed = md5(self.email + ''.join(random.choice(string.lowercase) for i in range(64))).digest().encode('base64')[:-1]  # strip last character because its a newline
            self.username = "******".format(hashed[:25])

        if getattr(settings, 'BADGEUSER_SKIP_LAST_LOGIN_TIME', True):
            # skip saving last_login to the database
            if 'update_fields' in kwargs and kwargs['update_fields'] is not None and 'last_login' in kwargs['update_fields']:
                kwargs['update_fields'].remove('last_login')
                if len(kwargs['update_fields']) < 1:
                    # nothing to do, abort so we dont call .publish()
                    return
        return super(BadgeUser, self).save(*args, **kwargs)
Example #3
0
class BadgeUser(BaseVersionedEntity, AbstractUser, cachemodel.CacheModel):
    """
    A full-featured user model that can be an Earner, Issuer, or Consumer of Open Badges
    """
    entity_class_name = 'BadgeUser'

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    badgrapp = models.ForeignKey('mainsite.BadgrApp', blank=True, null=True, default=None, on_delete=models.SET_NULL)

    marketing_opt_in = models.BooleanField(default=False)

    objects = BadgeUserManager()

    class Meta:
        verbose_name = _('badge user')
        verbose_name_plural = _('badge users')
        db_table = 'users'

    def __str__(self):
        primary_identifier = self.email or next((e for e in self.all_verified_recipient_identifiers), '')
        return "{} ({})".format(self.get_full_name(), primary_identifier)

    def get_full_name(self):
        return "%s %s" % (self.first_name, self.last_name)

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.primary_email], **kwargs)

    def publish(self):
        super(BadgeUser, self).publish()
        self.publish_by('username')

    def delete(self, *args, **kwargs):
        cached_emails = self.cached_emails()
        if cached_emails.exists():
            for email in cached_emails:
                email.delete()
        super(BadgeUser, self).delete(*args, **kwargs)
        self.publish_delete('username')

    @cachemodel.cached_method(auto_publish=True)
    def cached_verified_urls(self):
        return [
            r.identifier for r in
            self.userrecipientidentifier_set.filter(
                verified=True, type=UserRecipientIdentifier.IDENTIFIER_TYPE_URL)]

    @cachemodel.cached_method(auto_publish=True)
    def cached_verified_phone_numbers(self):
        return [
            r.identifier for r in
            self.userrecipientidentifier_set.filter(
                verified=True, type=UserRecipientIdentifier.IDENTIFIER_TYPE_TELEPHONE)]

    @cachemodel.cached_method(auto_publish=True)
    def cached_emails(self):
        return CachedEmailAddress.objects.filter(user=self)

    @cachemodel.cached_method(auto_publish=True)
    def cached_backpackcollections(self):
        return BackpackCollection.objects.filter(created_by=self)

    @property
    def email_items(self):
        return self.cached_emails()

    @email_items.setter
    def email_items(self, value):
        """
        Update this users EmailAddress from a list of BadgeUserEmailSerializerV2 data
        :param value: list(BadgeUserEmailSerializerV2)
        :return: None
        """
        return self.set_email_items(value)

    def set_email_items(self, value, send_confirmations=True, allow_verify=False, is_privileged_user=False):
        """
            If value is empty and the user is privileged, assume they meant to remove the users email and allow it.
            E.g. They are preforming some sort of ELT use case
        """
        if is_privileged_user and len(value) == 0:
            for email_address in self.email_items:
                email_address.delete()
            self.email = ''
            self.save()

        else:
            if len(value) < 1:
                raise ValidationError("Must have at least 1 email")

            new_email_idx = {d['email']: d for d in value}

            primary_count = sum(1 if d.get('primary', False) else 0 for d in value)

            if primary_count != 1:
                raise ValidationError("Must have exactly 1 primary email")
            requested_primary = [d for d in value if d.get('primary', False)][0]

            with transaction.atomic():
                # add or update existing items
                for email_data in value:
                    primary = email_data.get('primary', False)
                    verified = email_data.get('verified', False)
                    emailaddress, created = CachedEmailAddress.cached.get_or_create(
                        email=email_data['email'],
                        defaults={
                            'user': self,
                            'primary': primary
                        })
                    if not created:
                        dirty = False

                        if emailaddress.user_id == self.id:
                            # existing email address owned by user
                            emailaddress.primary = primary
                            dirty = True
                        elif not emailaddress.verified:
                            # existing unverified email address, handover to this user
                            emailaddress.user = self
                            emailaddress.primary = primary
                            emailaddress.save()  # in this case, don't mark as dirty
                            emailaddress.send_confirmation()
                        else:
                            # existing email address used by someone else
                            raise ValidationError("Email '{}' may already be in use".format(email_data.get('email')))

                        if allow_verify and verified != emailaddress.verified:
                            emailaddress.verified = verified
                            dirty = True

                        if dirty:
                            emailaddress.save()
                    else:
                        # email is new
                        if allow_verify and email_data.get('verified') is True:
                            emailaddress.verified = True
                            emailaddress.save()
                        if emailaddress.verified is False and created is True and send_confirmations is True:
                            # new email address send a confirmation
                            emailaddress.send_confirmation()

                    if not emailaddress.verified:
                        continue  # only verified email addresses may have variants. Don't bother trying otherwise.

                    requested_variants = email_data.get('cached_variant_emails', [])
                    existing_variant_emails = emailaddress.cached_variant_emails()
                    for requested_variant in requested_variants:
                        if requested_variant not in existing_variant_emails:
                            EmailAddressVariant.objects.create(
                                canonical_email=emailaddress, email=requested_variant
                            )

                # remove old items
                for emailaddress in self.email_items:
                    if emailaddress.email.lower() not in (lower_case_idx.lower() for lower_case_idx in new_email_idx):
                        emailaddress.delete()

            if self.email != requested_primary:
                self.email = requested_primary['email']
                self.save()


    def cached_email_variants(self):
        return chain.from_iterable(email.cached_variants() for email in self.cached_emails())

    def can_add_variant(self, email):
        try:
            canonical_email = CachedEmailAddress.objects.get(email=email, user=self, verified=True)
        except CachedEmailAddress.DoesNotExist:
            return False

        if email != canonical_email.email \
                and email not in [e.email for e in canonical_email.cached_variants()] \
                and EmailAddressVariant(email=email, canonical_email=canonical_email).is_valid():
            return True
        return False

    @property
    def primary_email(self):
        primaries = [e for e in self.cached_emails() if e.primary]
        if len(primaries) > 0:
            return primaries[0].email
        return self.email

    @property
    def verified_emails(self):
        return [e for e in self.cached_emails() if e.verified]

    @property
    def verified(self):
        if self.is_superuser:
            return True

        if len(self.all_verified_recipient_identifiers) > 0:
            return True

        return False

    @property
    def all_recipient_identifiers(self):
        return [e.email for e in self.cached_emails()] + \
            [e.email for e in self.cached_email_variants()] + \
            self.cached_verified_urls() + \
            self.cached_verified_phone_numbers()

    @property
    def all_verified_recipient_identifiers(self):
        return ([e.email for e in self.cached_emails() if e.verified]
                + [e.email for e in self.cached_email_variants()]
                + self.cached_verified_urls()
                + self.cached_verified_phone_numbers())

    def is_email_verified(self, email):
        if email in self.all_verified_recipient_identifiers:
            return True

        try:
            app_infos = ApplicationInfo.objects.filter(application__user=self)
            if any(app_info.trust_email_verification for app_info in app_infos):
                return True
        except ApplicationInfo.DoesNotExist:
            return False

        return False

    @cachemodel.cached_method(auto_publish=True)
    def cached_issuers(self):
        return Issuer.objects.filter(staff__id=self.id).distinct()

    @property
    def peers(self):
        """
        a BadgeUser is a Peer of another BadgeUser if they appear in an IssuerStaff together
        """
        return set(chain(*[[s.cached_user for s in i.cached_issuerstaff()] for i in self.cached_issuers()]))

    def cached_badgeclasses(self):
        return chain.from_iterable(issuer.cached_badgeclasses() for issuer in self.cached_issuers())

    @cachemodel.cached_method(auto_publish=True)
    def cached_badgeinstances(self):
        return BadgeInstance.objects.filter(recipient_identifier__in=self.all_recipient_identifiers)

    @cachemodel.cached_method(auto_publish=True)
    def cached_externaltools(self):
        return [a.cached_externaltool for a in self.externaltooluseractivation_set.filter(is_active=True)]

    @cachemodel.cached_method(auto_publish=True)
    def cached_token(self):
        user_token, created = \
                Token.objects.get_or_create(user=self)
        return user_token.key

    @cachemodel.cached_method(auto_publish=True)
    def cached_agreed_terms_version(self):
        try:
            return self.termsagreement_set.all().order_by('-terms_version')[0]
        except IndexError:
            pass
        return None

    @property
    def agreed_terms_version(self):
        v = self.cached_agreed_terms_version()
        if v is None:
            return 0
        return v.terms_version

    @agreed_terms_version.setter
    def agreed_terms_version(self, value):
        try:
            value = int(value)
        except ValueError as e:
            return

        if value > self.agreed_terms_version:
            if TermsVersion.active_objects.filter(version=value).exists():
                if not self.pk:
                    self.save()
                self.termsagreement_set.get_or_create(terms_version=value, defaults=dict(agreed=True))

    def replace_token(self):
        Token.objects.filter(user=self).delete()
        # user_token, created = \
        #         Token.objects.get_or_create(user=self)
        self.save()
        return self.cached_token()

    def save(self, *args, **kwargs):
        if not self.username:
            self.username = generate_badgr_username(self.email)

        if getattr(settings, 'BADGEUSER_SKIP_LAST_LOGIN_TIME', True):
            # skip saving last_login to the database
            if 'update_fields' in kwargs and kwargs['update_fields'] is not None and 'last_login' in kwargs['update_fields']:
                kwargs['update_fields'].remove('last_login')
                if len(kwargs['update_fields']) < 1:
                    # nothing to do, abort so we dont call .publish()
                    return
        return super(BadgeUser, self).save(*args, **kwargs)
Example #4
0
class BadgeUser(UserCachedObjectGetterMixin, UserPermissionsMixin,
                AbstractUser, BaseVersionedEntity):
    """
    A full-featured user model that can be an Earner, Issuer, or Consumer of Open Badges
    """
    entity_class_name = 'BadgeUser'
    is_teacher = models.BooleanField(default=False)
    invited = models.BooleanField(default=False)

    # The validated name from eduID
    validated_name = models.CharField(_('validated name'),
                                      max_length=254,
                                      blank=True,
                                      null=True)

    # Override from AbstractUser
    first_name = models.CharField(_('first name'), max_length=254, blank=True)
    last_name = models.CharField(_('last name'), max_length=254, blank=True)

    badgrapp = models.ForeignKey('mainsite.BadgrApp',
                                 on_delete=models.SET_NULL,
                                 blank=True,
                                 null=True,
                                 default=None)
    is_staff = models.BooleanField(
        _('Backend-staff member'),
        default=False,
        help_text=_(
            'Designates whether the user can log into this admin site.'),
    )

    # canvas LTI id
    lti_id = models.CharField(unique=True,
                              max_length=50,
                              default=None,
                              null=True,
                              blank=True,
                              help_text='LTI user id, unique per user')
    marketing_opt_in = models.BooleanField(default=False)

    objects = BadgeUserManager()

    institution = models.ForeignKey('institution.Institution',
                                    on_delete=models.CASCADE,
                                    null=True)

    class Meta:
        verbose_name = _('badge user')
        verbose_name_plural = _('badge users')
        db_table = 'users'
        permissions = (
            ('view_issuer_tab', 'User can view Issuer tab in front end'),
            ('view_management_tab', 'User can view Management dashboard'),
            ('has_faculty_scope', 'User has faculty scope'),
            ('has_institution_scope', 'User has institution scope'),
            ('ui_issuer_add', 'User can add issuer in front end'),
        )

    def __unicode__(self):
        return "{} <{}>".format(self.get_full_name(), self.email)

    @property
    def email_items(self):
        return self.cached_emails()

    @email_items.setter
    def email_items(self, value):
        """
        Update this users EmailAddress from a list of BadgeUserEmailSerializerV2 data
        :param value: list(BadgeUserEmailSerializerV2)
        :return: None
        """
        if len(value) < 1:
            raise ValidationError("Must have at least 1 email")

        new_email_idx = {d['email']: d for d in value}

        primary_count = sum(1 if d.get('primary', False) else 0 for d in value)
        if primary_count != 1:
            raise ValidationError("Must have exactly 1 primary email")

        with transaction.atomic():
            # add or update existing items
            for email_data in value:
                primary = email_data.get('primary', False)
                emailaddress, created = CachedEmailAddress.cached.get_or_create(
                    email=email_data['email'],
                    defaults={
                        'user': self,
                        'primary': primary
                    })
                if created:
                    # new email address send a confirmation
                    emailaddress.send_confirmation()
                else:
                    if emailaddress.user_id == self.id:
                        # existing email address owned by user
                        emailaddress.primary = primary
                        emailaddress.save()
                    elif not emailaddress.verified:
                        # existing unverified email address, handover to this user
                        emailaddress.user = self
                        emailaddress.primary = primary
                        emailaddress.save()
                        emailaddress.send_confirmation()
                    else:
                        # existing email address used by someone else
                        raise ValidationError(
                            "Email '{}' may already be in use".format(
                                email_data.get('email')))

            # remove old items
            for emailaddress in self.email_items:
                if emailaddress.email not in new_email_idx:
                    emailaddress.delete()

    @property
    def current_symmetric_key(self):
        return self.symmetrickey_set.get(current=True)

    def accept_terms(self, terms):
        agreement, _ = TermsAgreement.objects.get_or_create(user=self,
                                                            terms=terms)
        agreement.agreed_version = terms.version
        agreement.agreed = True
        agreement.save()

    def accept_general_terms(self):
        general_terms = Terms.get_general_terms(self)
        for terms in general_terms:
            self.accept_terms(terms)
        self.remove_cached_data(['cached_terms_agreements'])

    def general_terms_accepted(self):
        nr_accepted = 0
        general_terms = Terms.get_general_terms(self)
        for general_term in general_terms:
            if general_term.has_been_accepted_by(self):
                nr_accepted += 1
        return general_terms.__len__() == nr_accepted

    @property
    def full_name(self):
        return self.get_full_name()

    def get_full_name(self):
        if self.first_name and self.last_name:
            return "%s %s" % (self.first_name, self.last_name)
        else:
            return ''

    def clear_affiliations(self):
        """Removes all affiliations"""
        affiliations = StudentAffiliation.objects.filter(user=self)
        for affiliation in affiliations:
            affiliation.delete()
        self.remove_cached_data(['cached_affiliations'])

    def add_affiliations(self, affiliations):
        """
        param: affiliations: list of dicts [{'eppn': <eppn>m 'schac_home': <schac_home>}]
        """
        for affiliation in affiliations:
            StudentAffiliation.objects.get_or_create(user=self, **affiliation)
        self.remove_cached_data(['cached_affiliations'])

    @property
    def eppns(self):
        return [aff.eppn for aff in self.cached_affiliations()]

    @property
    def schac_homes(self):
        if self.is_teacher:
            return [self.institution.identifier]
        return [aff.schac_home for aff in self.cached_affiliations()]

    @property
    def direct_awards(self):
        return DirectAward.objects.filter(eppn__in=self.eppns,
                                          status='Unaccepted')

    def match_provisionments(self):
        """Used to match provisions on initial login"""
        provisions = UserProvisionment.objects.filter(
            email=self.email, for_teacher=self.is_teacher)
        for provision in provisions:
            provision.match_user(self)
        return provisions

    def email_user(self, subject, html_message):
        """
        Sends an email to this User.
        """
        if settings.LOCAL_DEVELOPMENT_MODE:
            open_mail_in_browser(html_message)
        try:
            EmailBlacklist.objects.get(email=self.primary_email)
        except EmailBlacklist.DoesNotExist:
            # Allow sending, as this email is not blacklisted.
            plain_text = strip_tags(html_message)
            send_mail(subject,
                      message=plain_text,
                      html_message=html_message,
                      recipient_list=[self.primary_email])
        else:
            return
            # TODO: Report email non-delivery somewhere.

    def publish(self):
        super(BadgeUser, self).publish()
        self.publish_by('username')

    def delete(self, *args, **kwargs):
        super(BadgeUser, self).delete(*args, **kwargs)
        cache.clear()

    def can_add_variant(self, email):
        try:
            canonical_email = CachedEmailAddress.objects.get(email=email,
                                                             user=self,
                                                             verified=True)
        except CachedEmailAddress.DoesNotExist:
            return False

        if email != canonical_email.email \
                and email not in [e.email for e in canonical_email.cached_variants()] \
                and EmailAddressVariant(email=email, canonical_email=canonical_email).is_valid():
            return True
        return False

    @property
    def primary_email(self):
        primaries = [e for e in self.cached_emails() if e.primary]
        if len(primaries) > 0:
            return primaries[0].email
        return self.email

    @property
    def verified_emails(self):
        return [e for e in self.cached_emails() if e.verified]

    @property
    def verified(self):
        if self.is_superuser:
            return True

        if len(self.verified_emails) > 0:
            return True

        return False

    @property
    def all_recipient_identifiers(self):
        return [self.get_recipient_identifier()]

    def get_recipient_identifier(self):
        from allauth.socialaccount.models import SocialAccount
        try:
            account = SocialAccount.objects.get(user=self.pk)
            return account.uid
        except SocialAccount.DoesNotExist:
            return None

    def get_social_account(self):
        from allauth.socialaccount.models import SocialAccount
        try:
            account = SocialAccount.objects.get(user=self.pk)
            return account
        except SocialAccount.DoesNotExist:
            return None

    @property
    def is_student(self):
        return not self.is_teacher

    def get_assertions_ready_for_signing(self):
        assertion_timestamps = AssertionTimeStamp.objects.filter(
            signer=self).exclude(proof='')
        return [
            ts.badge_instance for ts in assertion_timestamps
            if ts.badge_instance.signature == None
        ]

    def replace_token(self):
        Token.objects.filter(user=self).delete()
        # user_token, created = \
        #         Token.objects.get_or_create(user=self)
        self.save()
        return self.cached_token()

    def save(self, *args, **kwargs):
        if not self.username:
            self.username = generate_badgr_username(self.email)

        if getattr(settings, 'BADGEUSER_SKIP_LAST_LOGIN_TIME', True):
            # skip saving last_login to the database
            if 'update_fields' in kwargs and kwargs[
                    'update_fields'] is not None and 'last_login' in kwargs[
                        'update_fields']:
                kwargs['update_fields'].remove('last_login')
                if len(kwargs['update_fields']) < 1:
                    # nothing to do, abort so we dont call .publish()
                    return
        return super(BadgeUser, self).save(*args, **kwargs)