models.py 19 KB

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