usermodel.py 20 KB

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