Source code for pretalx.person.models.user

import json
import random
from hashlib import md5

import pytz
from django.conf import settings
from django.contrib.auth.models import (
    AbstractBaseUser, BaseUserManager, PermissionsMixin,
)
from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction
from django.db.models import Q
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import get_language, override, ugettext_lazy as _
from rest_framework.authtoken.models import Token

from pretalx.common.urls import build_absolute_uri


class UserManager(BaseUserManager):
    """The user manager class."""

    def create_user(self, password: str = None, **kwargs):
        user = self.model(**kwargs)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, password: str, **kwargs):
        user = self.create_user(password=password, **kwargs)
        user.is_staff = True
        user.is_administrator = True
        user.is_superuser = False
        user.save(update_fields=['is_staff', 'is_administrator', 'is_superuser'])
        return user


def assign_code(obj, length=6):
    # This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
    # and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
    # handwriting (2/Z, 4/A, 5/S, 6/G).
    while True:
        code = get_random_string(length=length, allowed_chars=User.CODE_CHARSET)

        if not User.objects.filter(code__iexact=code).exists():
            obj.code = code
            return code


[docs]class User(PermissionsMixin, AbstractBaseUser): """ The pretalx user model. Users describe all kinds of persons who interact with pretalx: Organisers, reviewers, submitters, speakers. :param code: A user's alphanumeric code is autogenerated, may not be changed, and is the unique identifier of that user. :param name: A name fit for public display. Will be used in the user interface and for public display for all speakers in all of their events. :param password: The password is stored using Django's PasswordField. Use the ``set_password`` and ``check_password`` methods to interact with it. :param nick: The nickname field has been deprecated and is scheduled to be deleted. Use the email field instead. :param groups: Django internals, not used in pretalx. :param user_permissions: Django internals, not used in pretalx. """ EMAIL_FIELD = 'email' USERNAME_FIELD = 'email' CODE_CHARSET = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789') objects = UserManager() code = models.CharField(max_length=16, unique=True, null=True) nick = models.CharField(max_length=60, null=True, blank=True) name = models.CharField( max_length=120, verbose_name=_('Name'), help_text=_('Please enter the name you wish to be displayed publicly. This name will be used for all events you are participating in on this server.'), ) email = models.EmailField( unique=True, verbose_name=_('E-Mail'), help_text=_( 'Your email address will be used for password resets and notification about your event/submissions.' ), ) is_active = models.BooleanField(default=True, help_text='Inactive users are not allowed to log in.') is_staff = models.BooleanField(default=False, help_text='A default Django flag. Not in use in pretalx.') is_administrator = models.BooleanField(default=False, help_text='Should only be ``True`` for people with administrative access to the server pretalx runs on.') is_superuser = models.BooleanField(default=False, help_text='Never set this flag to ``True``, since it short-circuits all authorization mechanisms.') locale = models.CharField( max_length=32, default=settings.LANGUAGE_CODE, choices=settings.LANGUAGES, verbose_name=_('Preferred language'), ) timezone = models.CharField( choices=[(tz, tz) for tz in pytz.common_timezones], max_length=30, default='UTC' ) avatar = models.ImageField( null=True, blank=True, verbose_name=_('Profile picture'), help_text=_('If possible, upload an image that is least 120 pixels wide.'), ) get_gravatar = models.BooleanField( default=False, verbose_name=_('Retrieve profile picture via gravatar'), help_text=_( 'If you have registered with an email address that has a gravatar account, we can retrieve your profile picture from there.' ), ) pw_reset_token = models.CharField(null=True, max_length=160, verbose_name='Password reset token') pw_reset_time = models.DateTimeField(null=True, verbose_name='Password reset time') def __str__(self) -> str: """Not for public consumption as it includes the email address.""" return self.name + f' <{self.email}>' if self.name else self.email or str(_('Unnamed user'))
[docs] def get_display_name(self) -> str: """Returns a user's name or 'Unnamed user'.""" return self.name if self.name else str(_('Unnamed user'))
def save(self, *args, **kwargs): self.email = self.email.lower().strip() if not self.code: assign_code(self) return super().save(args, kwargs)
[docs] def event_profile(self, event): """Retrieve (and/or create) the event :class:`~pretalx.person.models.profile.SpeakerProfile` for this user. :type event: :class:`pretalx.event.models.event.Event` :retval: :class:`pretalx.person.models.profile.EventProfile` """ from pretalx.person.models.profile import SpeakerProfile profile = self.profiles.select_related('event').filter(event=event).first() if profile: return profile profile = SpeakerProfile(event=event, user=self) if self.pk: profile.save() return profile
[docs] def log_action(self, action: str, data: dict=None, person=None, orga: bool=False): """Create a log entry for this user. :param action: The log action that took place. :param data: Addition data to be saved. :param person: The person modifying this user. Defaults to this user. :type person: :class:`~pretalx.person.models.user.User` :param orga: Was this action initiated by a privileged user?""" from pretalx.common.models import ActivityLog if data: data = json.dumps(data) ActivityLog.objects.create( person=person or self, content_object=self, action_type=action, data=data, is_orga_action=orga, )
def logged_actions(self): """Returns all log entries that were made about this user.""" from pretalx.common.models import ActivityLog return ActivityLog.objects.filter( content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk, ) def own_actions(self): """Returns all log entries that were made by this user.""" from pretalx.common.models import ActivityLog return ActivityLog.objects.filter(person=self) def deactivate(self): """Delete the user by unsetting all of their information.""" from pretalx.submission.models import Answer self.email = f'deleted_user_{random.randint(0, 999)}@localhost' while self.__class__.objects.filter(email__iexact=self.email).exists(): self.email = f'deleted_user_{random.randint(0, 999)}' self.name = 'Deleted User' self.is_active = False self.is_superuser = False self.is_administrator = False self.locale = 'en' self.timezone = 'UTC' self.pw_reset_token = None self.pw_reset_time = None self.save() self.profiles.all().update(biography='') Answer.objects.filter( person=self, question__contains_personal_data=True ).delete() for team in self.teams.all(): team.members.remove(self) @cached_property def gravatar_parameter(self) -> str: return md5(self.email.strip().encode()).hexdigest() @cached_property def has_avatar(self) -> bool: return self.get_gravatar or self.has_local_avatar @cached_property def has_local_avatar(self) -> bool: return self.avatar and self.avatar != 'False'
[docs] def get_events_with_any_permission(self): """Returns a queryset of events for which this user has any type of permission.""" from pretalx.event.models import Event if self.is_administrator: return Event.objects.all() return Event.objects.filter( Q( organiser_id__in=self.teams.filter(all_events=True).values_list( 'organiser', flat=True ) ) | Q(id__in=self.teams.values_list('limit_events__id', flat=True)) )
[docs] def get_events_for_permission(self, **kwargs): """Returns a queryset of events for which this user as all of the given permissions. Permissions are given as named arguments, e.g. ``get_events_for_permission(is_reviewer=True)``.""" from pretalx.event.models import Event if self.is_administrator: return Event.objects.all() orga_teams = self.teams.filter(**kwargs) absolute = orga_teams.filter(all_events=True).values_list( 'organiser', flat=True ) relative = orga_teams.filter(all_events=False).values_list( 'limit_events', flat=True ) return Event.objects.filter( models.Q(organiser__in=absolute) | models.Q(pk__in=relative) ).distinct()
[docs] def get_permissions_for_event(self, event) -> set: """Returns a set of all permission a user has for the given event. :type event: :class:`~pretalx.event.models.event.Event`""" if self.is_administrator: return { 'can_create_events', 'can_change_teams', 'can_change_organiser_settings', 'can_change_event_settings', 'can_change_submissions', 'is_reviewer', } teams = event.teams.filter(members__in=[self]) if not teams: return set() return set().union(*[team.permission_set for team in teams])
def remaining_override_votes(self, event) -> int: """Returns the amount of override votes a user may still give in reviews in the given event. :type event: :class:`~pretalx.event.models.event.Event` """ allowed = max( event.teams.filter(members__in=[self], is_reviewer=True).values_list( 'review_override_votes', flat=True ) or [0] ) overridden = self.reviews.filter( submission__event=event, override_vote__isnull=False ).count() return max(allowed - overridden, 0) def regenerate_token(self) -> Token: """Generates a new API access token, deleting the old one.""" self.log_action(action='pretalx.user.token.reset') Token.objects.filter(user=self).delete() return Token.objects.create(user=self) @transaction.atomic def reset_password(self, event, user=None): from pretalx.mail.models import QueuedMail self.pw_reset_token = get_random_string(32) self.pw_reset_time = now() self.save() context = { 'name': self.name or '', 'url': build_absolute_uri( 'orga:auth.recover', kwargs={'token': self.pw_reset_token} ), } mail_text = _( '''Hi {name}, you have requested a new password for your pretalx account. To reset your password, click on the following link: {url} If this wasn\'t you, you can just ignore this email. All the best, the pretalx robot''' ) with override(get_language()): mail = QueuedMail.objects.create( subject=_('Password recovery'), text=str(mail_text).format(**context), ) mail.to_users.add(self) mail.send() self.log_action( action='pretalx.user.password.reset', person=user, orga=bool(user) )