usermodel.py 20 KB

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