usermodel.py 20 KB

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