user.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. from hashlib import md5
  2. from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
  3. from django.contrib.auth.models import UserManager as BaseUserManager
  4. from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
  5. from django.contrib.auth.password_validation import validate_password
  6. from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
  7. from django.core.mail import send_mail
  8. from django.db import IntegrityError, models, transaction
  9. from django.urls import reverse
  10. from django.utils import timezone
  11. from django.utils.translation import gettext_lazy as _
  12. from misago.acl.models import Role
  13. from misago.conf import settings
  14. from misago.core.pgutils import PgPartialIndex
  15. from misago.core.utils import slugify
  16. from misago.users import avatars
  17. from misago.users.audittrail import create_user_audit_trail
  18. from misago.users.signatures import is_user_signature_valid
  19. from misago.users.utils import hash_email
  20. from .rank import Rank
  21. class UserManager(BaseUserManager):
  22. @transaction.atomic
  23. def create_user(
  24. self, username, email, password=None, create_audit_trail=False,
  25. joined_from_ip=None, set_default_avatar=False, **extra_fields
  26. ):
  27. from misago.users.validators import validate_email, validate_username
  28. email = self.normalize_email(email)
  29. username = self.model.normalize_username(username)
  30. if not email:
  31. raise ValueError(_("User must have an email address."))
  32. WATCH_DICT = {
  33. 'no': self.model.SUBSCRIBE_NONE,
  34. 'watch': self.model.SUBSCRIBE_NOTIFY,
  35. 'watch_email': self.model.SUBSCRIBE_ALL,
  36. }
  37. if not 'subscribe_to_started_threads' in extra_fields:
  38. new_value = WATCH_DICT[settings.subscribe_start]
  39. extra_fields['subscribe_to_started_threads'] = new_value
  40. if not 'subscribe_to_replied_threads' in extra_fields:
  41. new_value = WATCH_DICT[settings.subscribe_reply]
  42. extra_fields['subscribe_to_replied_threads'] = new_value
  43. extra_fields.update({'is_staff': False, 'is_superuser': False})
  44. now = timezone.now()
  45. user = self.model(
  46. last_login=now,
  47. joined_on=now,
  48. joined_from_ip=joined_from_ip,
  49. **extra_fields
  50. )
  51. user.set_username(username)
  52. user.set_email(email)
  53. user.set_password(password)
  54. validate_username(username)
  55. validate_email(email)
  56. if not 'rank' in extra_fields:
  57. user.rank = Rank.objects.get_default()
  58. user.save(using=self._db)
  59. if set_default_avatar:
  60. avatars.set_default_avatar(
  61. user, settings.default_avatar, settings.default_gravatar_fallback
  62. )
  63. else:
  64. # just for test purposes
  65. user.avatars = [{'size': 400, 'url': 'http://placekitten.com/400/400'}]
  66. authenticated_role = Role.objects.get(special_role='authenticated')
  67. if authenticated_role not in user.roles.all():
  68. user.roles.add(authenticated_role)
  69. user.update_acl_key()
  70. user.save(update_fields=['avatars', 'acl_key'])
  71. if create_audit_trail:
  72. create_user_audit_trail(user, user.joined_from_ip, user)
  73. # populate online tracker with default value
  74. Online.objects.create(user=user, last_click=now)
  75. return user
  76. @transaction.atomic
  77. def create_superuser(self, username, email, password, set_default_avatar=False):
  78. user = self.create_user(
  79. username,
  80. email,
  81. password=password,
  82. set_default_avatar=set_default_avatar,
  83. )
  84. try:
  85. user.rank = Rank.objects.get(name=_("Forum team"))
  86. user.update_acl_key()
  87. except Rank.DoesNotExist:
  88. pass
  89. user.is_staff = True
  90. user.is_superuser = True
  91. updated_fields = ('rank', 'acl_key', 'is_staff', 'is_superuser')
  92. user.save(update_fields=updated_fields, using=self._db)
  93. return user
  94. def get_by_username(self, username):
  95. return self.get(slug=slugify(username))
  96. def get_by_email(self, email):
  97. return self.get(email_hash=hash_email(email))
  98. def get_by_username_or_email(self, login):
  99. if '@' in login:
  100. return self.get(email_hash=hash_email(login))
  101. return self.get(slug=slugify(login))
  102. class User(AbstractBaseUser, PermissionsMixin):
  103. ACTIVATION_NONE = 0
  104. ACTIVATION_USER = 1
  105. ACTIVATION_ADMIN = 2
  106. SUBSCRIBE_NONE = 0
  107. SUBSCRIBE_NOTIFY = 1
  108. SUBSCRIBE_ALL = 2
  109. SUBSCRIBE_CHOICES = [
  110. (SUBSCRIBE_NONE, _("No")),
  111. (SUBSCRIBE_NOTIFY, _("Notify")),
  112. (SUBSCRIBE_ALL, _("Notify with e-mail")),
  113. ]
  114. LIMIT_INVITES_TO_NONE = 0
  115. LIMIT_INVITES_TO_FOLLOWED = 1
  116. LIMIT_INVITES_TO_NOBODY = 2
  117. LIMIT_INVITES_TO_CHOICES = [
  118. (LIMIT_INVITES_TO_NONE, _("Everybody")),
  119. (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
  120. (LIMIT_INVITES_TO_NOBODY, _("Nobody")),
  121. ]
  122. # Note that "username" field is purely for shows.
  123. # When searching users by their names, always use lowercased string
  124. # and slug field instead that is normalized around DB engines
  125. # differences in case handling.
  126. username = models.CharField(max_length=30)
  127. slug = models.CharField(max_length=30, unique=True)
  128. # Misago stores user email in two fields:
  129. # "email" holds normalized email address
  130. # "email_hash" is lowercase hash of email address used to identify account
  131. # as well as enforcing on database level that no more than one user can be
  132. # using one email address
  133. email = models.EmailField(max_length=255, db_index=True)
  134. email_hash = models.CharField(max_length=32, unique=True)
  135. joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
  136. joined_from_ip = models.GenericIPAddressField(null=True, blank=True)
  137. is_hiding_presence = models.BooleanField(default=False)
  138. rank = models.ForeignKey(
  139. 'Rank',
  140. null=True,
  141. blank=True,
  142. on_delete=models.deletion.PROTECT,
  143. )
  144. title = models.CharField(max_length=255, null=True, blank=True)
  145. requires_activation = models.PositiveIntegerField(default=ACTIVATION_NONE)
  146. is_staff = models.BooleanField(
  147. _('staff status'),
  148. default=False,
  149. help_text=_('Designates whether the user can log into admin sites.'),
  150. )
  151. roles = models.ManyToManyField('misago_acl.Role')
  152. acl_key = models.CharField(max_length=12, null=True, blank=True)
  153. is_active = models.BooleanField(
  154. _('active'),
  155. db_index=True,
  156. default=True,
  157. help_text=_(
  158. "Designates whether this user should be treated as active. "
  159. "Unselect this instead of deleting accounts."
  160. ),
  161. )
  162. is_active_staff_message = models.TextField(null=True, blank=True)
  163. is_deleting_account = models.BooleanField(default=False)
  164. avatar_tmp = models.ImageField(
  165. max_length=255,
  166. upload_to=avatars.store.upload_to,
  167. null=True,
  168. blank=True,
  169. )
  170. avatar_src = models.ImageField(
  171. max_length=255,
  172. upload_to=avatars.store.upload_to,
  173. null=True,
  174. blank=True,
  175. )
  176. avatar_crop = models.CharField(max_length=255, null=True, blank=True)
  177. avatars = JSONField(null=True, blank=True)
  178. is_avatar_locked = models.BooleanField(default=False)
  179. avatar_lock_user_message = models.TextField(null=True, blank=True)
  180. avatar_lock_staff_message = models.TextField(null=True, blank=True)
  181. signature = models.TextField(null=True, blank=True)
  182. signature_parsed = models.TextField(null=True, blank=True)
  183. signature_checksum = models.CharField(max_length=64, null=True, blank=True)
  184. is_signature_locked = models.BooleanField(default=False)
  185. signature_lock_user_message = models.TextField(null=True, blank=True)
  186. signature_lock_staff_message = models.TextField(null=True, blank=True)
  187. followers = models.PositiveIntegerField(default=0)
  188. following = models.PositiveIntegerField(default=0)
  189. follows = models.ManyToManyField(
  190. 'self',
  191. related_name='followed_by',
  192. symmetrical=False,
  193. )
  194. blocks = models.ManyToManyField(
  195. 'self',
  196. related_name='blocked_by',
  197. symmetrical=False,
  198. )
  199. limits_private_thread_invites_to = models.PositiveIntegerField(
  200. default=LIMIT_INVITES_TO_NONE,
  201. choices=LIMIT_INVITES_TO_CHOICES,
  202. )
  203. unread_private_threads = models.PositiveIntegerField(default=0)
  204. sync_unread_private_threads = models.BooleanField(default=False)
  205. subscribe_to_started_threads = models.PositiveIntegerField(
  206. default=SUBSCRIBE_NONE,
  207. choices=SUBSCRIBE_CHOICES,
  208. )
  209. subscribe_to_replied_threads = models.PositiveIntegerField(
  210. default=SUBSCRIBE_NONE,
  211. choices=SUBSCRIBE_CHOICES,
  212. )
  213. threads = models.PositiveIntegerField(default=0)
  214. posts = models.PositiveIntegerField(default=0, db_index=True)
  215. last_posted_on = models.DateTimeField(null=True, blank=True)
  216. profile_fields = HStoreField(default=dict)
  217. agreements = ArrayField(models.PositiveIntegerField(), default=list)
  218. USERNAME_FIELD = 'slug'
  219. REQUIRED_FIELDS = ['email']
  220. objects = UserManager()
  221. class Meta:
  222. indexes = [
  223. PgPartialIndex(
  224. fields=['is_staff'],
  225. where={'is_staff': True},
  226. ),
  227. PgPartialIndex(
  228. fields=['requires_activation'],
  229. where={'requires_activation__gt': 0},
  230. ),
  231. PgPartialIndex(
  232. fields=['is_deleting_account'],
  233. where={'is_deleting_account': True},
  234. ),
  235. ]
  236. def clean(self):
  237. self.username = self.normalize_username(self.username)
  238. self.email = UserManager.normalize_email(self.email)
  239. def lock(self):
  240. """locks user in DB, shortcut for locking user model in views"""
  241. return User.objects.select_for_update().get(pk=self.pk)
  242. def delete(self, *args, **kwargs):
  243. if kwargs.pop('delete_content', False):
  244. self.delete_content()
  245. self.anonymize_data()
  246. avatars.delete_avatar(self)
  247. return super().delete(*args, **kwargs)
  248. def delete_content(self):
  249. from misago.users.signals import delete_user_content
  250. delete_user_content.send(sender=self)
  251. def mark_for_delete(self):
  252. self.is_active = False
  253. self.is_deleting_account = True
  254. self.save(update_fields=['is_active', 'is_deleting_account'])
  255. def anonymize_data(self):
  256. """Replaces username with anonymized one, then send anonymization signal.
  257. Items associated with this user then anonymize their user-specific data
  258. like username or IP addresses.
  259. """
  260. self.username = settings.MISAGO_ANONYMOUS_USERNAME
  261. self.slug = slugify(self.username)
  262. from misago.users.signals import anonymize_user_data
  263. anonymize_user_data.send(sender=self)
  264. @property
  265. def requires_activation_by_admin(self):
  266. return self.requires_activation == self.ACTIVATION_ADMIN
  267. @property
  268. def requires_activation_by_user(self):
  269. return self.requires_activation == self.ACTIVATION_USER
  270. @property
  271. def can_be_messaged_by_everyone(self):
  272. preference = self.limits_private_thread_invites_to
  273. return preference == self.LIMIT_INVITES_TO_NONE
  274. @property
  275. def can_be_messaged_by_followed(self):
  276. preference = self.limits_private_thread_invites_to
  277. return preference == self.LIMIT_INVITES_TO_FOLLOWED
  278. @property
  279. def can_be_messaged_by_nobody(self):
  280. preference = self.limits_private_thread_invites_to
  281. return preference == self.LIMIT_INVITES_TO_NOBODY
  282. @property
  283. def has_valid_signature(self):
  284. return is_user_signature_valid(self)
  285. def get_absolute_url(self):
  286. return reverse(
  287. 'misago:user', kwargs={
  288. 'slug': self.slug,
  289. 'pk': self.pk,
  290. }
  291. )
  292. def get_username(self):
  293. """dirty hack: return real username instead of normalized slug"""
  294. return self.username
  295. def get_full_name(self):
  296. return self.username
  297. def get_short_name(self):
  298. return self.username
  299. def get_real_name(self):
  300. return self.profile_fields.get('real_name')
  301. def set_username(self, new_username, changed_by=None):
  302. new_username = self.normalize_username(new_username)
  303. if new_username != self.username:
  304. old_username = self.username
  305. self.username = new_username
  306. self.slug = slugify(new_username)
  307. if self.pk:
  308. changed_by = changed_by or self
  309. namechange = self.record_name_change(
  310. changed_by, new_username, old_username
  311. )
  312. from misago.users.signals import username_changed
  313. username_changed.send(sender=self)
  314. return namechange
  315. def record_name_change(self, changed_by, new_username, old_username):
  316. return self.namechanges.create(
  317. new_username=new_username,
  318. old_username=old_username,
  319. changed_by=changed_by,
  320. changed_by_username=changed_by.username,
  321. )
  322. def set_email(self, new_email):
  323. self.email = UserManager.normalize_email(new_email)
  324. self.email_hash = hash_email(new_email)
  325. def get_any_title(self):
  326. return self.title or self.rank.title or self.rank.name
  327. def get_roles(self):
  328. roles_pks = []
  329. roles_dict = {}
  330. for role in self.roles.all():
  331. roles_pks.append(role.pk)
  332. role.origin = self
  333. roles_dict[role.pk] = role
  334. if self.rank:
  335. for role in self.rank.roles.all():
  336. if role.pk not in roles_pks:
  337. role.origin = self.rank
  338. roles_pks.append(role.pk)
  339. roles_dict[role.pk] = role
  340. return [roles_dict[r] for r in sorted(roles_pks)]
  341. def update_acl_key(self):
  342. roles_pks = []
  343. for role in self.get_roles():
  344. if role.origin == 'self':
  345. roles_pks.append('u%s' % role.pk)
  346. else:
  347. roles_pks.append('%s:%s' % (self.rank.pk, role.pk))
  348. self.acl_key = md5(','.join(roles_pks).encode()).hexdigest()[:12]
  349. def email_user(self, subject, message, from_email=None, **kwargs):
  350. """sends an email to this user (for compat with Django)"""
  351. send_mail(subject, message, from_email, [self.email], **kwargs)
  352. def is_following(self, user_or_id):
  353. try:
  354. user_id = user_or_id.id
  355. except AttributeError:
  356. user_id = user_or_id
  357. try:
  358. self.follows.get(id=user_id)
  359. return True
  360. except User.DoesNotExist:
  361. return False
  362. def is_blocking(self, user_or_id):
  363. try:
  364. user_id = user_or_id.id
  365. except AttributeError:
  366. user_id = user_or_id
  367. try:
  368. self.blocks.get(id=user_id)
  369. return True
  370. except User.DoesNotExist:
  371. return False
  372. class Online(models.Model):
  373. user = models.OneToOneField(
  374. settings.AUTH_USER_MODEL,
  375. primary_key=True,
  376. related_name='online_tracker',
  377. on_delete=models.CASCADE,
  378. )
  379. last_click = models.DateTimeField(default=timezone.now)
  380. def save(self, *args, **kwargs):
  381. try:
  382. super().save(*args, **kwargs)
  383. except IntegrityError:
  384. pass # first come is first serve in online tracker
  385. class UsernameChange(models.Model):
  386. user = models.ForeignKey(
  387. settings.AUTH_USER_MODEL,
  388. related_name='namechanges',
  389. on_delete=models.CASCADE,
  390. )
  391. changed_by = models.ForeignKey(
  392. settings.AUTH_USER_MODEL,
  393. null=True,
  394. blank=True,
  395. related_name='user_renames',
  396. on_delete=models.SET_NULL,
  397. )
  398. changed_by_username = models.CharField(max_length=30)
  399. changed_on = models.DateTimeField(default=timezone.now)
  400. new_username = models.CharField(max_length=255)
  401. old_username = models.CharField(max_length=255)
  402. class Meta:
  403. get_latest_by = "changed_on"
  404. def set_change_author(self, user):
  405. self.changed_by = user
  406. self.changed_by_username = user.username
  407. class AnonymousUser(DjangoAnonymousUser):
  408. acl_key = 'anonymous'
  409. @property
  410. def acl_cache(self):
  411. raise Exception("AnonymousUser.acl_cache has been removed")
  412. @acl_cache.setter
  413. def acl_cache(self, value):
  414. raise TypeError("AnonymousUser instances can't be made ACL aware")
  415. def get_roles(self):
  416. try:
  417. return [Role.objects.get(special_role="anonymous")]
  418. except Role.DoesNotExist:
  419. raise RuntimeError("Anonymous user role not found.")
  420. def update_acl_key(self):
  421. raise TypeError("Can't update ACL key on anonymous users")