models.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import hashlib
  2. import math
  3. from random import choice
  4. from django.conf import settings
  5. from django.contrib.auth.hashers import (
  6. check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
  7. from django.core.exceptions import ValidationError
  8. from django.core.mail import EmailMultiAlternatives
  9. from django.db import models, connection, transaction
  10. from django.template import RequestContext
  11. from django.utils import timezone as tz_util
  12. from django.utils.translation import ugettext_lazy as _
  13. from misago.monitor.monitor import Monitor
  14. from misago.security import get_random_string
  15. from misago.settings.settings import Settings as DBSettings
  16. from misago.users.validators import validate_username, validate_password, validate_email
  17. from misago.utils import slugify
  18. from path import path
  19. class UserManager(models.Manager):
  20. """
  21. User Manager provides us with some additional methods for users
  22. """
  23. def get_blank_user(self):
  24. blank_user = User(
  25. join_date=tz_util.now(),
  26. join_ip='127.0.0.1'
  27. )
  28. return blank_user
  29. def resync_monitor(self, monitor):
  30. monitor['users'] = self.count()
  31. monitor['users_inactive'] = self.filter(activation__gt=0).count()
  32. last_user = self.latest('id')
  33. monitor['last_user'] = last_user.pk
  34. monitor['last_user_name'] = last_user.username
  35. monitor['last_user_slug'] = last_user.username_slug
  36. def create_user(self, username, email, password, group, timezone=False, ip='127.0.0.1', activation=0, request=False):
  37. token = ''
  38. if activation > 0:
  39. token = get_random_string(12)
  40. if timezone == False:
  41. try:
  42. timezone = request.settings['default_timezone']
  43. db_settings = request.settings
  44. except AttributeError:
  45. db_settings = DBSettings()
  46. timezone = db_settings['default_timezone']
  47. # Store user in database
  48. new_user = User(
  49. join_date=tz_util.now(),
  50. join_ip=ip,
  51. activation=activation,
  52. token=token,
  53. timezone=timezone,
  54. group=group,
  55. )
  56. new_user.set_username(username)
  57. new_user.set_email(email)
  58. new_user.set_password(password)
  59. new_user.full_clean()
  60. new_user.default_avatar(db_settings)
  61. new_user.save(force_insert=True)
  62. # Set second group and default avatar
  63. new_user.groups.add(group)
  64. new_user.save(force_update=True)
  65. # Load monitor
  66. try:
  67. monitor = request.monitor
  68. except AttributeError:
  69. monitor = Monitor()
  70. # Update forum stats
  71. if activation == 0:
  72. monitor['users'] = int(monitor['users']) + 1
  73. monitor['last_user'] = new_user.pk
  74. monitor['last_user_name'] = new_user.username
  75. monitor['last_user_slug'] = new_user.username_slug
  76. else:
  77. monitor['users_inactive'] = int(monitor['users_inactive']) + 1
  78. # Return new user
  79. return new_user
  80. def get_by_email(self, email):
  81. return self.get(email_hash=hashlib.md5(email).hexdigest())
  82. def filter_overview(self, start, end):
  83. return self.filter(join_date__gte=start).filter(join_date__lte=end)
  84. class User(models.Model):
  85. """
  86. Misago User model
  87. """
  88. username = models.CharField(max_length=255,validators=[validate_username])
  89. username_slug = models.SlugField(max_length=255,unique=True,
  90. error_messages={'unique': _("This user name is already in use by another user.")})
  91. email = models.EmailField(max_length=255,validators=[validate_email])
  92. email_hash = models.CharField(max_length=32,unique=True,
  93. error_messages={'unique': _("This email address is already in use by another user.")})
  94. password = models.CharField(max_length=255)
  95. password_date = models.DateTimeField()
  96. avatar_type = models.CharField(max_length=10,null=True,blank=True)
  97. avatar_image = models.CharField(max_length=255,null=True,blank=True)
  98. signature = models.TextField(null=True,blank=True)
  99. signature_preparsed = models.TextField(null=True,blank=True)
  100. join_date = models.DateTimeField()
  101. join_ip = models.GenericIPAddressField()
  102. join_agent = models.TextField(null=True,blank=True)
  103. last_date = models.DateTimeField(null=True,blank=True)
  104. last_ip = models.GenericIPAddressField(null=True,blank=True)
  105. last_agent = models.TextField(null=True,blank=True)
  106. hide_activity = models.BooleanField(default=False)
  107. topics = models.PositiveIntegerField(default=0)
  108. topics_delta = models.IntegerField(default=0)
  109. posts = models.PositiveIntegerField(default=0)
  110. posts_delta = models.IntegerField(default=0)
  111. karma = models.IntegerField(default=0)
  112. karma_delta = models.IntegerField(default=0)
  113. followers = models.PositiveIntegerField(default=0)
  114. followers_delta = models.IntegerField(default=0)
  115. score = models.FloatField(default=0,db_index=True)
  116. rank = models.ForeignKey('Rank',null=True,blank=True,db_index=True,on_delete=models.SET_NULL)
  117. last_post = models.DateTimeField(null=True,blank=True)
  118. last_search = models.DateTimeField(null=True,blank=True)
  119. alerts = models.PositiveIntegerField(default=0)
  120. alerts_new = models.PositiveIntegerField(default=0)
  121. activation = models.IntegerField(default=0)
  122. token = models.CharField(max_length=12,null=True,blank=True)
  123. avatar_ban = models.BooleanField(default=False)
  124. avatar_ban_reason_user = models.TextField(null=True,blank=True)
  125. avatar_ban_reason_admin = models.TextField(null=True,blank=True)
  126. avatar_ban_expires = models.DateTimeField(null=True,blank=True)
  127. signature_ban = models.BooleanField(default=False)
  128. signature_ban_reason_user = models.TextField(null=True,blank=True)
  129. signature_ban_reason_admin = models.TextField(null=True,blank=True)
  130. signature_ban_expires = models.DateTimeField(null=True,blank=True)
  131. timezone = models.CharField(max_length=255,default='utc')
  132. roles = models.CommaSeparatedIntegerField(max_length=255,null=True,blank=True)
  133. group = models.ForeignKey('Group',on_delete=models.PROTECT)
  134. groups = models.ManyToManyField('Group',related_name='groups')
  135. acl_cache = models.TextField(null=True,blank=True)
  136. objects = UserManager()
  137. ACTIVATION_NONE = 0
  138. ACTIVATION_USER = 1
  139. ACTIVATION_ADMIN = 2
  140. ACTIVATION_PASSWORD = 3
  141. statistics_name = _('Users Registrations')
  142. def is_admin(self):
  143. return 1 == self.group.pk
  144. def is_anonymous(self):
  145. return False
  146. def is_authenticated(self):
  147. return True
  148. def is_crawler(self):
  149. return False
  150. def default_avatar(self, db_settings):
  151. if db_settings['default_avatar'] == 'gallery':
  152. try:
  153. avatars_list = []
  154. try:
  155. # First try, _default path
  156. galleries = path(settings.STATICFILES_DIRS[0]).joinpath('avatars').joinpath('_default')
  157. avatars_list += galleries.files('*.gif')
  158. avatars_list += galleries.files('*.jpg')
  159. avatars_list += galleries.files('*.jpeg')
  160. avatars_list += galleries.files('*.png')
  161. except Exception as e:
  162. pass
  163. # Second try, all paths
  164. if not avatars_list:
  165. avatars_list = []
  166. for directory in path(settings.STATICFILES_DIRS[0]).joinpath('avatars').dirs():
  167. avatars_list += directory.files('*.gif')
  168. avatars_list += directory.files('*.jpg')
  169. avatars_list += directory.files('*.jpeg')
  170. avatars_list += directory.files('*.png')
  171. if avatars_list:
  172. # Pick random avatar from list
  173. self.avatar_type = 'gallery'
  174. self.avatar_image = '/'.join(path(choice(avatars_list)).splitall()[-2:])
  175. return True
  176. except Exception as e:
  177. pass
  178. self.avatar_type = 'gravatar'
  179. self.avatar_image = None
  180. return True
  181. def delete_avatar(self):
  182. if self.avatar_type == 'upload':
  183. # DELETE OUR AVATAR!!!
  184. pass
  185. def delete(self, *args, **kwargs):
  186. self.delete_avatar()
  187. super(User, self).delete(*args, **kwargs)
  188. def set_username(self, username):
  189. self.username = username.strip()
  190. self.username_slug = slugify(username)
  191. def is_username_valid(self, e):
  192. try:
  193. raise ValidationError(e.message_dict['username'])
  194. except KeyError:
  195. pass
  196. try:
  197. raise ValidationError(e.message_dict['username_slug'])
  198. except KeyError:
  199. pass
  200. def is_email_valid(self, e):
  201. try:
  202. raise ValidationError(e.message_dict['email'])
  203. except KeyError:
  204. pass
  205. try:
  206. raise ValidationError(e.message_dict['email_hash'])
  207. except KeyError:
  208. pass
  209. def is_password_valid(self, e):
  210. try:
  211. raise ValidationError(e.message_dict['password'])
  212. except KeyError:
  213. pass
  214. def set_email(self, email):
  215. self.email = email.strip().lower()
  216. self.email_hash = hashlib.md5(self.email).hexdigest()
  217. def set_password(self, raw_password):
  218. self.password_date = tz_util.now()
  219. self.password = make_password(raw_password.strip())
  220. def set_last_visit(self, ip, agent, hidden=False):
  221. self.last_date = tz_util.now()
  222. self.last_ip = ip
  223. self.last_agent = agent
  224. self.last_hide = hidden
  225. def check_password(self, raw_password, mobile=False):
  226. """
  227. Returns a boolean of whether the raw_password was correct. Handles
  228. hashing formats behind the scenes.
  229. """
  230. def setter(raw_password):
  231. self.set_password(raw_password)
  232. self.save()
  233. # Is standard password allright?
  234. if check_password(raw_password, self.password, setter):
  235. return True
  236. # Check mobile password?
  237. if mobile:
  238. raw_password = raw_password[:1].lower() + raw_password[1:]
  239. else:
  240. password_reversed = u''
  241. for c in raw_password:
  242. r = c.upper()
  243. if r == c:
  244. r = c.lower()
  245. password_reversed += r
  246. raw_password = password_reversed
  247. return check_password(raw_password, self.password, setter)
  248. def get_avatar(self, size='normal'):
  249. # Get uploaded avatar
  250. if self.avatar_type == 'upload':
  251. return settings.MEDIA_URL + 'avatars/' + self.avatar_image
  252. # Get gallery avatar
  253. if self.avatar_type == 'gallery':
  254. return settings.STATIC_URL + 'avatars/' + self.avatar_image
  255. # No avatar found, get gravatar
  256. if size == 'big':
  257. size = 150;
  258. elif size == 'small':
  259. size = 64;
  260. elif size == 'tiny':
  261. size = 46;
  262. else:
  263. size = 100
  264. return 'http://www.gravatar.com/avatar/%s?s=%s' % (hashlib.md5(self.email).hexdigest(), size)
  265. def email_user(self, request, template, subject, context={}):
  266. templates = request.theme.get_email_templates(template)
  267. context = RequestContext(request, context)
  268. context['author'] = context['user']
  269. context['user'] = self
  270. # Set message recipient
  271. if settings.DEBUG:
  272. recipient = settings.CATCH_ALL_EMAIL_ADDRESS
  273. else:
  274. recipient = self.email
  275. # Build and send message
  276. email = EmailMultiAlternatives(subject, templates[0].render(context), settings.EMAIL_HOST_USER, [recipient])
  277. email.attach_alternative(templates[1].render(context), "text/html")
  278. email.send()
  279. def get_date(self):
  280. return self.join_date
  281. class Guest(object):
  282. """
  283. Misago Guest dummy
  284. """
  285. def is_admin(self):
  286. return False
  287. def is_anonymous(self):
  288. return True
  289. def is_authenticated(self):
  290. return False
  291. def is_crawler(self):
  292. return False
  293. class Crawler(object):
  294. """
  295. Misago Crawler dummy
  296. """
  297. def __init__(self, username):
  298. self.username = username
  299. def is_admin(self):
  300. return False
  301. def is_anonymous(self):
  302. return True
  303. def is_authenticated(self):
  304. return False
  305. def is_crawler(self):
  306. return True
  307. class Group(models.Model):
  308. """
  309. Misago Users Group model
  310. """
  311. name = models.CharField(max_length=255)
  312. name_slug = models.SlugField(max_length=255)
  313. hidden = models.BooleanField(default=False)
  314. tab = models.CharField(max_length=255,null=True,blank=True)
  315. position = models.IntegerField(default=0)
  316. rank = models.ForeignKey('Rank',null=True,blank=True,db_index=True,on_delete=models.SET_NULL)
  317. special = models.BooleanField(default=False)
  318. class Rank(models.Model):
  319. """
  320. Misago User Rank
  321. Ranks are ready style/title pairs that are assigned to users either by admin (special ranks) or as result of user activity.
  322. """
  323. name = models.CharField(max_length=255,null=True,blank=True)
  324. name_slug = models.CharField(max_length=255,null=True,blank=True)
  325. description = models.TextField(null=True,blank=True)
  326. style = models.CharField(max_length=255,null=True,blank=True)
  327. title = models.CharField(max_length=255,null=True,blank=True)
  328. special = models.BooleanField(default=False)
  329. as_tab = models.BooleanField(default=False)
  330. order = models.IntegerField(default=0)
  331. criteria = models.CharField(max_length=255,default='')
  332. def assign_rank(self, users=0, special_ranks=None):
  333. if not self.criteria or self.special or users == 0:
  334. # Rank cant be rolled in
  335. return False
  336. if self.criteria == "0":
  337. # Just update all fellows
  338. User.objects.exclude(rank__in=special_ranks).update(rank=self)
  339. else:
  340. # Count number of users to update
  341. if self.criteria[-1] == '%':
  342. criteria = int(self.criteria[0:-1])
  343. criteria = int(math.ceil(float(users / 100.0)* criteria))
  344. else:
  345. criteria = int(self.criteria)
  346. # Join special ranks
  347. if special_ranks:
  348. special_ranks = ','.join(special_ranks)
  349. # Run raw query
  350. cursor = connection.cursor()
  351. try:
  352. # Postgresql
  353. if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
  354. or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
  355. if special_ranks:
  356. cursor.execute('''UPDATE users_user
  357. FROM (
  358. SELECT id
  359. FROM users_user
  360. WHERE rank_id NOT IN (%s)
  361. ORDER BY score DESC LIMIT %s
  362. ) AS updateable
  363. SET rank_id=%s
  364. WHERE id = updateable.id
  365. RETURNING *''' % (self.id, special_ranks, criteria))
  366. else:
  367. cursor.execute('''UPDATE users_user
  368. FROM (
  369. SELECT id
  370. FROM users_user
  371. ORDER BY score DESC LIMIT %s
  372. ) AS updateable
  373. SET rank_id=%s
  374. WHERE id = updateable.id
  375. RETURNING *''', [self.id, criteria])
  376. # MySQL, SQLite and Oracle
  377. if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.mysql'
  378. or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3'
  379. or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.oracle'):
  380. if special_ranks:
  381. cursor.execute('''UPDATE users_user
  382. SET rank_id=%s
  383. WHERE rank_id NOT IN (%s)
  384. ORDER BY score DESC
  385. LIMIT %s''' % (self.id, special_ranks, criteria))
  386. else:
  387. cursor.execute('''UPDATE users_user
  388. SET rank_id=%s
  389. ORDER BY score DESC
  390. LIMIT %s''', [self.id, criteria])
  391. except Exception as e:
  392. print 'Error updating users ranking: %s' % e
  393. transaction.commit_unless_managed()
  394. return True
  395. class Follower(models.Model):
  396. """
  397. Misago Users follow model
  398. """
  399. user = models.ForeignKey('User',related_name='+')
  400. target = models.ForeignKey('User')
  401. since = models.DateTimeField()
  402. class Foe(models.Model):
  403. """
  404. Misago Users foes model
  405. """
  406. user = models.ForeignKey('User')
  407. target = models.ForeignKey('User',related_name='+')
  408. since = models.DateTimeField()