1import json
2import random
3from hashlib import md5
4
5import pytz
6from django.conf import settings
7from django.contrib.auth.models import AbstractBaseUser
8from django.contrib.auth.models import BaseUserManager
9from django.contrib.auth.models import PermissionsMixin
10from django.contrib.contenttypes.models import ContentType
11from django.db import models
12from django.db import transaction
13from django.db.models import Q
14from django.utils.crypto import get_random_string
15from django.utils.functional import cached_property
16from django.utils.timezone import now
17from django.utils.translation import get_language
18from django.utils.translation import override
19from django.utils.translation import ugettext_lazy as _
20from pretalx.common.urls import build_absolute_uri
21from rest_framework.authtoken.models import Token
22
23
24class UserManager(BaseUserManager):
25 """The user manager class."""
26
27 def create_user(self, password: str = None, **kwargs):
28 user = self.model(**kwargs)
29 user.set_password(password)
30 user.save()
31 return user
32
33 def create_superuser(self, password: str, **kwargs):
34 user = self.create_user(password=password, **kwargs)
35 user.is_staff = True
36 user.is_administrator = True
37 user.is_superuser = False
38 user.save(update_fields=["is_staff", "is_administrator", "is_superuser"])
39 return user
40
41
42def assign_code(obj, length=6):
43 # This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
44 # and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
45 # handwriting (2/Z, 4/A, 5/S, 6/G).
46 while True:
47 code = get_random_string(length=length, allowed_chars=User.CODE_CHARSET)
48
49 if not User.objects.filter(code__iexact=code).exists():
50 obj.code = code
51 return code
52
53
54class User(PermissionsMixin, AbstractBaseUser):
55 """
56 The pretalx user model.
57
58 Users describe all kinds of persons who interact with pretalx: Organisers, reviewers, submitters, speakers.
59
60 :param code: A user's alphanumeric code is autogenerated, may not be
61 changed, and is the unique identifier of that user.
62 :param name: A name fit for public display. Will be used in the user
63 interface and for public display for all speakers in all of their
64 events.
65 :param password: The password is stored using Django's PasswordField. Use
66 the ``set_password`` and ``check_password`` methods to interact with it.
67 :param nick: The nickname field has been deprecated and is scheduled to be
68 deleted. Use the email field instead.
69 :param groups: Django internals, not used in pretalx.
70 :param user_permissions: Django internals, not used in pretalx.
71 """
72
73 EMAIL_FIELD = "email"
74 USERNAME_FIELD = "email"
75 CODE_CHARSET = list("ABCDEFGHJKLMNPQRSTUVWXYZ3789")
76
77 objects = UserManager()
78
79 code = models.CharField(max_length=16, unique=True, null=True)
80 nick = models.CharField(max_length=60, null=True, blank=True)
81 name = models.CharField(
82 max_length=120,
83 verbose_name=_("Name"),
84 help_text=_(
85 "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."
86 ),
87 )
88 email = models.EmailField(
89 unique=True,
90 verbose_name=_("E-Mail"),
91 help_text=_(
92 "Your email address will be used for password resets and notification about your event/submissions."
93 ),
94 )
95 is_active = models.BooleanField(
96 default=True, help_text="Inactive users are not allowed to log in."
97 )
98 is_staff = models.BooleanField(
99 default=False, help_text="A default Django flag. Not in use in pretalx."
100 )
101 is_administrator = models.BooleanField(
102 default=False,
103 help_text="Should only be ``True`` for people with administrative access to the server pretalx runs on.",
104 )
105 is_superuser = models.BooleanField(
106 default=False,
107 help_text="Never set this flag to ``True``, since it short-circuits all authorization mechanisms.",
108 )
109 locale = models.CharField(
110 max_length=32,
111 default=settings.LANGUAGE_CODE,
112 choices=settings.LANGUAGES,
113 verbose_name=_("Preferred language"),
114 )
115 timezone = models.CharField(
116 choices=[(tz, tz) for tz in pytz.common_timezones], max_length=30, default="UTC"
117 )
118 avatar = models.ImageField(
119 null=True,
120 blank=True,
121 verbose_name=_("Profile picture"),
122 help_text=_("If possible, upload an image that is least 120 pixels wide."),
123 )
124 get_gravatar = models.BooleanField(
125 default=False,
126 verbose_name=_("Retrieve profile picture via gravatar"),
127 help_text=_(
128 "If you have registered with an email address that has a gravatar account, we can retrieve your profile picture from there."
129 ),
130 )
131 pw_reset_token = models.CharField(
132 null=True, max_length=160, verbose_name="Password reset token"
133 )
134 pw_reset_time = models.DateTimeField(null=True, verbose_name="Password reset time")
135
136 def __str__(self) -> str:
137 """Not for public consumption as it includes the email address."""
138 return (
139 self.name + f" <{self.email}>"
140 if self.name
141 else self.email or str(_("Unnamed user"))
142 )
143
144 def get_display_name(self) -> str:
145 """Returns a user's name or 'Unnamed user'."""
146 return self.name if self.name else str(_("Unnamed user"))
147
148 def save(self, *args, **kwargs):
149 self.email = self.email.lower().strip()
150 if not self.code:
151 assign_code(self)
152 return super().save(args, kwargs)
153
154 def event_profile(self, event):
155 """Retrieve (and/or create) the event :class:`~pretalx.person.models.profile.SpeakerProfile` for this user.
156
157 :type event: :class:`pretalx.event.models.event.Event`
158 :retval: :class:`pretalx.person.models.profile.EventProfile`
159 """
160 from pretalx.person.models.profile import SpeakerProfile
161
162 profile = self.profiles.select_related("event").filter(event=event).first()
163 if profile:
164 return profile
165 profile = SpeakerProfile(event=event, user=self)
166 if self.pk:
167 profile.save()
168 return profile
169
170 def log_action(
171 self, action: str, data: dict = None, person=None, orga: bool = False
172 ):
173 """Create a log entry for this user.
174
175 :param action: The log action that took place.
176 :param data: Addition data to be saved.
177 :param person: The person modifying this user. Defaults to this user.
178 :type person: :class:`~pretalx.person.models.user.User`
179 :param orga: Was this action initiated by a privileged user?"""
180 from pretalx.common.models import ActivityLog
181
182 if data:
183 data = json.dumps(data)
184
185 ActivityLog.objects.create(
186 person=person or self,
187 content_object=self,
188 action_type=action,
189 data=data,
190 is_orga_action=orga,
191 )
192
193 def logged_actions(self):
194 """Returns all log entries that were made about this user."""
195 from pretalx.common.models import ActivityLog
196
197 return ActivityLog.objects.filter(
198 content_type=ContentType.objects.get_for_model(type(self)),
199 object_id=self.pk,
200 )
201
202 def own_actions(self):
203 """Returns all log entries that were made by this user."""
204 from pretalx.common.models import ActivityLog
205
206 return ActivityLog.objects.filter(person=self)
207
208 def deactivate(self):
209 """Delete the user by unsetting all of their information."""
210 from pretalx.submission.models import Answer
211
212 self.email = f"deleted_user_{random.randint(0, 999)}@localhost"
213 while self.__class__.objects.filter(email__iexact=self.email).exists():
214 self.email = f"deleted_user_{random.randint(0, 999)}"
215 self.name = "Deleted User"
216 self.is_active = False
217 self.is_superuser = False
218 self.is_administrator = False
219 self.locale = "en"
220 self.timezone = "UTC"
221 self.pw_reset_token = None
222 self.pw_reset_time = None
223 self.save()
224 self.profiles.all().update(biography="")
225 Answer.objects.filter(
226 person=self, question__contains_personal_data=True
227 ).delete()
228 for team in self.teams.all():
229 team.members.remove(self)
230
231 @cached_property
232 def gravatar_parameter(self) -> str:
233 return md5(self.email.strip().encode()).hexdigest()
234
235 @cached_property
236 def has_avatar(self) -> bool:
237 return self.get_gravatar or self.has_local_avatar
238
239 @cached_property
240 def has_local_avatar(self) -> bool:
241 return self.avatar and self.avatar != "False"
242
243 def get_events_with_any_permission(self):
244 """Returns a queryset of events for which this user has any type of permission."""
245 from pretalx.event.models import Event
246
247 if self.is_administrator:
248 return Event.objects.all()
249
250 return Event.objects.filter(
251 Q(
252 organiser_id__in=self.teams.filter(all_events=True).values_list(
253 "organiser", flat=True
254 )
255 )
256 | Q(id__in=self.teams.values_list("limit_events__id", flat=True))
257 )
258
259 def get_events_for_permission(self, **kwargs):
260 """Returns a queryset of events for which this user as all of the given permissions.
261
262 Permissions are given as named arguments, e.g. ``get_events_for_permission(is_reviewer=True)``."""
263 from pretalx.event.models import Event
264
265 if self.is_administrator:
266 return Event.objects.all()
267
268 orga_teams = self.teams.filter(**kwargs)
269 absolute = orga_teams.filter(all_events=True).values_list(
270 "organiser", flat=True
271 )
272 relative = orga_teams.filter(all_events=False).values_list(
273 "limit_events", flat=True
274 )
275 return Event.objects.filter(
276 models.Q(organiser__in=absolute) | models.Q(pk__in=relative)
277 ).distinct()
278
279 def get_permissions_for_event(self, event) -> set:
280 """Returns a set of all permission a user has for the given event.
281
282 :type event: :class:`~pretalx.event.models.event.Event`"""
283 if self.is_administrator:
284 return {
285 "can_create_events",
286 "can_change_teams",
287 "can_change_organiser_settings",
288 "can_change_event_settings",
289 "can_change_submissions",
290 "is_reviewer",
291 }
292 teams = event.teams.filter(members__in=[self])
293 if not teams:
294 return set()
295 return set().union(*[team.permission_set for team in teams])
296
297 def remaining_override_votes(self, event) -> int:
298 """Returns the amount of override votes a user may still give in reviews in the given event.
299
300 :type event: :class:`~pretalx.event.models.event.Event`
301 """
302 allowed = max(
303 event.teams.filter(members__in=[self], is_reviewer=True).values_list(
304 "review_override_votes", flat=True
305 )
306 or [0]
307 )
308 overridden = self.reviews.filter(
309 submission__event=event, override_vote__isnull=False
310 ).count()
311 return max(allowed - overridden, 0)
312
313 def regenerate_token(self) -> Token:
314 """Generates a new API access token, deleting the old one."""
315 self.log_action(action="pretalx.user.token.reset")
316 Token.objects.filter(user=self).delete()
317 return Token.objects.create(user=self)
318
319 @transaction.atomic
320 def reset_password(self, event, user=None):
321 from pretalx.mail.models import QueuedMail
322
323 self.pw_reset_token = get_random_string(32)
324 self.pw_reset_time = now()
325 self.save()
326
327 context = {
328 "name": self.name or "",
329 "url": build_absolute_uri(
330 "orga:auth.recover", kwargs={"token": self.pw_reset_token}
331 ),
332 }
333 mail_text = _(
334 """Hi {name},
335
336you have requested a new password for your pretalx account.
337To reset your password, click on the following link:
338
339 {url}
340
341If this wasn\'t you, you can just ignore this email.
342
343All the best,
344the pretalx robot"""
345 )
346
347 with override(get_language()):
348 mail = QueuedMail.objects.create(
349 subject=_("Password recovery"), text=str(mail_text).format(**context)
350 )
351 mail.to_users.add(self)
352 mail.send()
353 self.log_action(
354 action="pretalx.user.password.reset", person=user, orga=bool(user)
355 )