usermodel.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. import hashlib
  2. from datetime import timedelta
  3. import math
  4. from random import choice
  5. from path import path
  6. from django.contrib.auth.hashers import (
  7. check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
  8. from django.core.cache import cache, InvalidCacheBackendError
  9. from django.core.exceptions import ValidationError
  10. from django.core.mail import EmailMultiAlternatives
  11. from django.db import models
  12. from django.template import RequestContext
  13. from django.utils import timezone as tz_util
  14. from django.utils.translation import ugettext_lazy as _
  15. from misago.acl.builder import acl
  16. from misago.conf import settings
  17. from misago.monitor import monitor, UpdatingMonitor
  18. from misago.signals import delete_user_content, rename_user, sync_user_profile
  19. from misago.template.loader import render_to_string
  20. from misago.utils.avatars import avatar_size
  21. from misago.utils.strings import random_string, slugify
  22. from misago.validators import validate_username, validate_password, validate_email
  23. class UserManager(models.Manager):
  24. """
  25. User Manager provides us with some additional methods for users
  26. """
  27. def get_blank_user(self):
  28. blank_user = User(
  29. join_date=tz_util.now(),
  30. join_ip='127.0.0.1'
  31. )
  32. return blank_user
  33. def resync_monitor(self):
  34. with UpdatingMonitor() as cm:
  35. monitor['users'] = self.filter(activation=0).count()
  36. monitor['users_inactive'] = self.filter(activation__gt=0).count()
  37. last_user = self.filter(activation=0).latest('id')
  38. monitor['last_user'] = last_user.pk
  39. monitor['last_user_name'] = last_user.username
  40. monitor['last_user_slug'] = last_user.username_slug
  41. def create_user(self, username, email, password, timezone=False, ip='127.0.0.1', agent='', no_roles=False, activation=0, request=False):
  42. token = ''
  43. if activation > 0:
  44. token = random_string(12)
  45. timezone = timezone or settings.default_timezone
  46. # Get first rank
  47. try:
  48. from misago.models import Rank
  49. default_rank = Rank.objects.filter(special=0).order_by('-order')[0]
  50. except IndexError:
  51. default_rank = None
  52. # Store user in database
  53. new_user = User(
  54. last_sync=tz_util.now(),
  55. join_date=tz_util.now(),
  56. join_ip=ip,
  57. join_agent=agent,
  58. activation=activation,
  59. token=token,
  60. timezone=timezone,
  61. rank=default_rank,
  62. subscribe_start=settings.subscribe_start,
  63. subscribe_reply=settings.subscribe_reply,
  64. )
  65. validate_username(username)
  66. validate_password(password)
  67. new_user.set_username(username)
  68. new_user.set_email(email)
  69. new_user.set_password(password)
  70. new_user.full_clean()
  71. new_user.default_avatar()
  72. new_user.save(force_insert=True)
  73. # Set user roles?
  74. if not no_roles:
  75. from misago.models import Role
  76. new_user.roles.add(Role.objects.get(_special='registered'))
  77. new_user.make_acl_key()
  78. new_user.save(force_update=True)
  79. # Update forum stats
  80. with UpdatingMonitor() as cm:
  81. if activation == 0:
  82. monitor.increase('users')
  83. monitor['last_user'] = new_user.pk
  84. monitor['last_user_name'] = new_user.username
  85. monitor['last_user_slug'] = new_user.username_slug
  86. else:
  87. monitor.increase('users_inactive')
  88. # Return new user
  89. return new_user
  90. def get_by_email(self, email):
  91. return self.get(email_hash=hashlib.md5(email).hexdigest())
  92. def filter_stats(self, start, end):
  93. return self.filter(join_date__gte=start).filter(join_date__lte=end)
  94. class User(models.Model):
  95. """
  96. Misago User model
  97. """
  98. username = models.CharField(max_length=255)
  99. username_slug = models.SlugField(max_length=255, unique=True,
  100. error_messages={'unique': _("This user name is already in use by another user.")})
  101. email = models.EmailField(max_length=255, validators=[validate_email])
  102. email_hash = models.CharField(max_length=32, unique=True,
  103. error_messages={'unique': _("This email address is already in use by another user.")})
  104. password = models.CharField(max_length=255)
  105. password_date = models.DateTimeField()
  106. avatar_type = models.CharField(max_length=10, null=True, blank=True)
  107. avatar_image = models.CharField(max_length=255, null=True, blank=True)
  108. avatar_original = models.CharField(max_length=255, null=True, blank=True)
  109. avatar_temp = models.CharField(max_length=255, null=True, blank=True)
  110. signature = models.TextField(null=True, blank=True)
  111. signature_preparsed = models.TextField(null=True, blank=True)
  112. join_date = models.DateTimeField()
  113. join_ip = models.GenericIPAddressField()
  114. join_agent = models.TextField(null=True, blank=True)
  115. last_date = models.DateTimeField(null=True, blank=True)
  116. last_ip = models.GenericIPAddressField(null=True, blank=True)
  117. last_agent = models.TextField(null=True, blank=True)
  118. hide_activity = models.PositiveIntegerField(default=0)
  119. subscribe_start = models.PositiveIntegerField(default=0)
  120. subscribe_reply = models.PositiveIntegerField(default=0)
  121. receive_newsletters = models.BooleanField(default=True)
  122. threads = models.PositiveIntegerField(default=0)
  123. posts = models.PositiveIntegerField(default=0)
  124. votes = models.PositiveIntegerField(default=0)
  125. karma_given_p = models.PositiveIntegerField(default=0)
  126. karma_given_n = models.PositiveIntegerField(default=0)
  127. karma_p = models.PositiveIntegerField(default=0)
  128. karma_n = models.PositiveIntegerField(default=0)
  129. following = models.PositiveIntegerField(default=0)
  130. followers = models.PositiveIntegerField(default=0)
  131. score = models.IntegerField(default=0)
  132. ranking = models.PositiveIntegerField(default=0)
  133. rank = models.ForeignKey('Rank', null=True, blank=True, on_delete=models.SET_NULL)
  134. last_sync = models.DateTimeField(null=True, blank=True)
  135. follows = models.ManyToManyField('self', related_name='follows_set', symmetrical=False)
  136. ignores = models.ManyToManyField('self', related_name='ignores_set', symmetrical=False)
  137. title = models.CharField(max_length=255, null=True, blank=True)
  138. last_post = models.DateTimeField(null=True, blank=True)
  139. last_search = models.DateTimeField(null=True, blank=True)
  140. alerts = models.PositiveIntegerField(default=0)
  141. alerts_date = models.DateTimeField(null=True, blank=True)
  142. allow_pds = models.PositiveIntegerField(default=0)
  143. unread_pds = models.PositiveIntegerField(default=0)
  144. sync_pds = models.BooleanField(default=False)
  145. activation = models.IntegerField(default=0)
  146. token = models.CharField(max_length=12, null=True, blank=True)
  147. avatar_ban = models.BooleanField(default=False)
  148. avatar_ban_reason_user = models.TextField(null=True, blank=True)
  149. avatar_ban_reason_admin = models.TextField(null=True, blank=True)
  150. signature_ban = models.BooleanField(default=False)
  151. signature_ban_reason_user = models.TextField(null=True, blank=True)
  152. signature_ban_reason_admin = models.TextField(null=True, blank=True)
  153. timezone = models.CharField(max_length=255, default='utc')
  154. roles = models.ManyToManyField('Role')
  155. is_team = models.BooleanField(default=False)
  156. acl_key = models.CharField(max_length=12, null=True, blank=True)
  157. objects = UserManager()
  158. ACTIVATION_NONE = 0
  159. ACTIVATION_USER = 1
  160. ACTIVATION_ADMIN = 2
  161. ACTIVATION_CREDENTIALS = 3
  162. statistics_name = _('Users Registrations')
  163. class Meta:
  164. app_label = 'misago'
  165. def is_god(self):
  166. try:
  167. return self.is_god_cache
  168. except AttributeError:
  169. for user in settings.ADMINS:
  170. if user[1].lower() == self.email:
  171. self.is_god_cache = True
  172. return True
  173. self.is_god_cache = False
  174. return False
  175. def is_anonymous(self):
  176. return False
  177. def is_authenticated(self):
  178. return True
  179. def is_crawler(self):
  180. return False
  181. def is_protected(self):
  182. for role in self.roles.all():
  183. if role.protected:
  184. return True
  185. return False
  186. def lock_avatar(self):
  187. # Kill existing avatar and lock our ability to change it
  188. self.delete_avatar()
  189. self.avatar_ban = True
  190. # Pick new one from _locked gallery
  191. galleries = path(settings.STATICFILES_DIRS[0]).joinpath('avatars').joinpath('_locked')
  192. avatars_list = galleries.files('*.gif')
  193. avatars_list += galleries.files('*.jpg')
  194. avatars_list += galleries.files('*.jpeg')
  195. avatars_list += galleries.files('*.png')
  196. self.avatar_type = 'gallery'
  197. self.avatar_image = '/'.join(path(choice(avatars_list)).splitall()[-2:])
  198. def default_avatar(self):
  199. if settings.default_avatar == 'gallery':
  200. try:
  201. avatars_list = []
  202. try:
  203. # First try, _default path
  204. galleries = path(settings.STATICFILES_DIRS[0]).joinpath('avatars').joinpath('_default')
  205. avatars_list += galleries.files('*.gif')
  206. avatars_list += galleries.files('*.jpg')
  207. avatars_list += galleries.files('*.jpeg')
  208. avatars_list += galleries.files('*.png')
  209. except Exception as e:
  210. pass
  211. # Second try, all paths
  212. if not avatars_list:
  213. avatars_list = []
  214. for directory in path(settings.STATICFILES_DIRS[0]).joinpath('avatars').dirs():
  215. if not directory[-7:] == '_locked' and not directory[-7:] == '_thumbs':
  216. avatars_list += directory.files('*.gif')
  217. avatars_list += directory.files('*.jpg')
  218. avatars_list += directory.files('*.jpeg')
  219. avatars_list += directory.files('*.png')
  220. if avatars_list:
  221. # Pick random avatar from list
  222. self.avatar_type = 'gallery'
  223. self.avatar_image = '/'.join(path(choice(avatars_list)).splitall()[-2:])
  224. return True
  225. except Exception as e:
  226. pass
  227. self.avatar_type = 'gravatar'
  228. self.avatar_image = None
  229. return True
  230. def delete_avatar_temp(self):
  231. if self.avatar_temp:
  232. try:
  233. av_file = path(settings.MEDIA_ROOT + 'avatars/' + self.avatar_temp)
  234. if not av_file.isdir():
  235. av_file.remove()
  236. except Exception:
  237. pass
  238. self.avatar_temp = None
  239. def delete_avatar_original(self):
  240. if self.avatar_original:
  241. try:
  242. av_file = path(settings.MEDIA_ROOT + 'avatars/' + self.avatar_original)
  243. if not av_file.isdir():
  244. av_file.remove()
  245. except Exception:
  246. pass
  247. self.avatar_original = None
  248. def delete_avatar_image(self):
  249. if self.avatar_image:
  250. for size in settings.AVATAR_SIZES[1:]:
  251. try:
  252. av_file = path(settings.MEDIA_ROOT + 'avatars/' + str(size) + '_' + self.avatar_image)
  253. if not av_file.isdir():
  254. av_file.remove()
  255. except Exception:
  256. pass
  257. try:
  258. av_file = path(settings.MEDIA_ROOT + 'avatars/' + self.avatar_image)
  259. if not av_file.isdir():
  260. av_file.remove()
  261. except Exception:
  262. pass
  263. self.avatar_image = None
  264. def delete_avatar(self):
  265. self.delete_avatar_temp()
  266. self.delete_avatar_original()
  267. self.delete_avatar_image()
  268. def delete_content(self):
  269. delete_user_content.send(sender=self)
  270. def delete(self, *args, **kwargs):
  271. self.delete_avatar()
  272. super(User, self).delete(*args, **kwargs)
  273. def set_username(self, username):
  274. self.username = username.strip()
  275. self.username_slug = slugify(username)
  276. def sync_username(self):
  277. rename_user.send(sender=self)
  278. def is_username_valid(self, e):
  279. try:
  280. raise ValidationError(e.message_dict['username'])
  281. except KeyError:
  282. pass
  283. try:
  284. raise ValidationError(e.message_dict['username_slug'])
  285. except KeyError:
  286. pass
  287. def is_email_valid(self, e):
  288. try:
  289. raise ValidationError(e.message_dict['email'])
  290. except KeyError:
  291. pass
  292. try:
  293. raise ValidationError(e.message_dict['email_hash'])
  294. except KeyError:
  295. pass
  296. def is_password_valid(self, e):
  297. try:
  298. raise ValidationError(e.message_dict['password'])
  299. except KeyError:
  300. pass
  301. def set_email(self, email):
  302. self.email = email.strip().lower()
  303. self.email_hash = hashlib.md5(self.email).hexdigest()
  304. def set_password(self, raw_password):
  305. self.password_date = tz_util.now()
  306. self.password = make_password(raw_password.strip())
  307. def set_last_visit(self, ip, agent):
  308. self.last_date = tz_util.now()
  309. self.last_ip = ip
  310. self.last_agent = agent
  311. def check_password(self, raw_password, mobile=False):
  312. """
  313. Returns a boolean of whether the raw_password was correct. Handles
  314. hashing formats behind the scenes.
  315. """
  316. def setter(raw_password):
  317. self.set_password(raw_password)
  318. self.save()
  319. # Is standard password allright?
  320. if check_password(raw_password, self.password, setter):
  321. return True
  322. # Check mobile password?
  323. if mobile:
  324. raw_password = raw_password[:1].lower() + raw_password[1:]
  325. else:
  326. password_reversed = u''
  327. for c in raw_password:
  328. r = c.upper()
  329. if r == c:
  330. r = c.lower()
  331. password_reversed += r
  332. raw_password = password_reversed
  333. return check_password(raw_password, self.password, setter)
  334. def is_following(self, user):
  335. try:
  336. return self.follows.filter(id=user.pk).count() > 0
  337. except AttributeError:
  338. return self.follows.filter(id=user).count() > 0
  339. def is_ignoring(self, user):
  340. try:
  341. return self.ignores.filter(id=user.pk).count() > 0
  342. except AttributeError:
  343. return self.ignores.filter(id=user).count() > 0
  344. def ignored_users(self):
  345. return [item['id'] for item in self.ignores.values('id')]
  346. def allow_pd_invite(self, user):
  347. # PD's from nobody
  348. if self.allow_pds == 3:
  349. return False
  350. # PD's from followed
  351. if self.allow_pds == 2:
  352. return self.is_following(user)
  353. # PD's from non-ignored
  354. if self.allow_pds == 1:
  355. return not self.is_ignoring(user)
  356. return True
  357. def get_roles(self):
  358. if self.rank:
  359. return self.roles.all() | self.rank.roles.all()
  360. return self.roles.all()
  361. def make_acl_key(self, force=False):
  362. if not force and self.acl_key:
  363. return self.acl_key
  364. roles_ids = []
  365. for role in self.roles.all():
  366. roles_ids.append(role.pk)
  367. for role in self.rank.roles.all():
  368. if not role.pk in roles_ids:
  369. roles_ids.append(role.pk)
  370. roles_ids.sort()
  371. self.acl_key = 'acl_%s' % hashlib.md5('_'.join(str(x) for x in roles_ids)).hexdigest()[0:8]
  372. self.save(update_fields=('acl_key',))
  373. return self.acl_key
  374. def acl(self, request):
  375. return acl(request, self)
  376. def get_avatar(self, size=None):
  377. image_size = avatar_size(size) if size else None
  378. # Get uploaded avatar
  379. if self.avatar_type == 'upload':
  380. image_prefix = '%s_' % image_size if image_size else ''
  381. return settings.MEDIA_URL + 'avatars/' + image_prefix + self.avatar_image
  382. # Get gallery avatar
  383. if self.avatar_type == 'gallery':
  384. image_prefix = '_thumbs/%s/' % image_size if image_size else ''
  385. return settings.STATIC_URL + 'avatars/' + image_prefix + self.avatar_image
  386. # No avatar found, get gravatar
  387. if not image_size:
  388. image_size = settings.AVATAR_SIZES[0]
  389. return 'http://www.gravatar.com/avatar/%s?s=%s' % (hashlib.md5(self.email).hexdigest(), image_size)
  390. def get_ranking(self):
  391. if not self.ranking:
  392. self.ranking = User.objects.filter(score__gt=self.score).count() + 1
  393. self.save(force_update=True)
  394. return self.ranking
  395. def get_title(self):
  396. if self.title:
  397. return self.title
  398. if self.rank:
  399. return self.rank.title
  400. return None
  401. def get_style(self):
  402. if self.rank:
  403. return self.rank.style
  404. return ''
  405. def email_user(self, request, template, subject, context={}):
  406. context = RequestContext(request, context)
  407. context['author'] = context['user']
  408. context['user'] = self
  409. email_html = render_to_string('_email/%s.html' % template,
  410. context_instance=context)
  411. email_text = render_to_string('_email/%s.txt' % template,
  412. context_instance=context)
  413. # Set message recipient
  414. if settings.DEBUG and settings.CATCH_ALL_EMAIL_ADDRESS:
  415. recipient = settings.CATCH_ALL_EMAIL_ADDRESS
  416. else:
  417. recipient = self.email
  418. # Build message and add it to queue
  419. email = EmailMultiAlternatives(subject, email_text, settings.EMAIL_HOST_USER, [recipient])
  420. email.attach_alternative(email_html, "text/html")
  421. request.mails_queue.append(email)
  422. def get_activation(self):
  423. activations = ['none', 'user', 'admin', 'credentials']
  424. return activations[self.activation]
  425. def alert(self, message):
  426. from misago.models import Alert
  427. self.alerts += 1
  428. return Alert(user=self, message=message, date=tz_util.now())
  429. def sync_unread_pds(self, unread):
  430. self.unread_pds = unread
  431. self.sync_pds = False
  432. def get_date(self):
  433. return self.join_date
  434. def sync_profile(self):
  435. if (settings.PROFILES_SYNC_FREQUENCY > 0 and
  436. self.last_sync <= tz_util.now() - timedelta(days=settings.PROFILES_SYNC_FREQUENCY)):
  437. sync_user_profile.send(sender=self)
  438. self.last_sync = tz_util.now()
  439. return True
  440. return False
  441. def timeline(self, qs, length=100):
  442. posts = {}
  443. now = tz_util.now()
  444. for item in qs.iterator():
  445. diff = (now - item.timeline_date).days
  446. try:
  447. posts[diff] += 1
  448. except KeyError:
  449. posts[diff] = 1
  450. graph = []
  451. for i in reversed(range(100)):
  452. try:
  453. graph.append(posts[i])
  454. except KeyError:
  455. graph.append(0)
  456. return graph
  457. class Guest(object):
  458. """
  459. Misago Guest dummy
  460. """
  461. id = -1
  462. pk = -1
  463. is_team = False
  464. def is_anonymous(self):
  465. return True
  466. def is_authenticated(self):
  467. return False
  468. def is_crawler(self):
  469. return False
  470. def get_roles(self):
  471. from misago.models import Role
  472. return Role.objects.filter(_special='guest')
  473. def make_acl_key(self):
  474. return 'acl_guest'
  475. class Crawler(Guest):
  476. """
  477. Misago Crawler dummy
  478. """
  479. is_team = False
  480. def __init__(self, username):
  481. self.username = username
  482. def is_anonymous(self):
  483. return False
  484. def is_authenticated(self):
  485. return False
  486. def is_crawler(self):
  487. return True
  488. """
  489. Signals handlers
  490. """
  491. def sync_user_handler(sender, **kwargs):
  492. sender.following = sender.follows.count()
  493. sender.followers = sender.follows_set.count()
  494. sync_user_profile.connect(sync_user_handler, dispatch_uid="sync_user_follows")