helpers.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils
  4. ~~~~~~~~~~~~~~~~~~~~
  5. A few utils that are used by flaskbb
  6. :copyright: (c) 2013 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import random
  10. import time
  11. from datetime import datetime, timedelta
  12. from flask import current_app
  13. from sqlalchemy import types
  14. from sqlalchemy.ext.mutable import Mutable
  15. from wtforms.widgets.core import Select, HTMLString, html_params
  16. from postmarkup import render_bbcode
  17. from flaskbb.extensions import redis
  18. def mark_online(user_id, guest=False):
  19. """
  20. Source: http://flask.pocoo.org/snippets/71/
  21. """
  22. now = int(time.time())
  23. expires = now + (current_app.config['ONLINE_LAST_MINUTES'] * 60) + 10
  24. if guest:
  25. all_users_key = 'online-guests/%d' % (now // 60)
  26. user_key = 'guest-activity/%s' % user_id
  27. else:
  28. all_users_key = 'online-users/%d' % (now // 60)
  29. user_key = 'user-activity/%s' % user_id
  30. p = redis.pipeline()
  31. p.sadd(all_users_key, user_id)
  32. p.set(user_key, now)
  33. p.expireat(all_users_key, expires)
  34. p.expireat(user_key, expires)
  35. p.execute()
  36. def get_last_user_activity(user_id, guest=False):
  37. if guest:
  38. last_active = redis.get('guest-activity/%s' % user_id)
  39. else:
  40. last_active = redis.get('user-activity/%s' % user_id)
  41. if last_active is None:
  42. return None
  43. return datetime.utcfromtimestamp(int(last_active))
  44. def get_online_users(guest=False):
  45. current = int(time.time()) // 60
  46. minutes = xrange(current_app.config['ONLINE_LAST_MINUTES'])
  47. if guest:
  48. return redis.sunion(['online-guests/%d' % (current - x)
  49. for x in minutes])
  50. return redis.sunion(['online-users/%d' % (current - x)
  51. for x in minutes])
  52. def check_perm(user, perm, forum, post_user_id=None):
  53. """
  54. Checks if the `user` has a specified `perm` in the `forum`
  55. If post_user_id is provided, it will also check if the user
  56. has created the post
  57. """
  58. if can_moderate(user, forum):
  59. return True
  60. if post_user_id and user.is_authenticated():
  61. return user.permissions[perm] and user.id == post_user_id
  62. return user.permissions[perm]
  63. def can_moderate(user, forum):
  64. """
  65. Checks if a user can moderate a forum
  66. He needs to be super moderator or a moderator of the
  67. specified `forum`
  68. """
  69. if user.permissions['mod'] and user.id in forum.moderators:
  70. return True
  71. return user.permissions['super_mod'] or user.permissions['admin']
  72. def perm_edit_post(user, post_user_id, forum):
  73. """
  74. Check if the post can be edited by the user
  75. """
  76. return check_perm(user=user, perm='editpost', forum=forum,
  77. post_user_id=post_user_id)
  78. def perm_delete_post(user, post_user_id, forum):
  79. """
  80. Check if the post can be deleted by the user
  81. """
  82. return check_perm(user=user, perm='deletepost', forum=forum,
  83. post_user_id=post_user_id)
  84. def perm_delete_topic(user, post_user_id, forum):
  85. """
  86. Check if the topic can be deleted by the user
  87. """
  88. return check_perm(user=user, perm='deletetopic', forum=forum,
  89. post_user_id=post_user_id)
  90. def perm_post_reply(user, forum):
  91. """
  92. Check if the user is allowed to post in the forum
  93. """
  94. return check_perm(user=user, perm='postreply', forum=forum)
  95. def perm_post_topic(user, forum):
  96. """
  97. Check if the user is allowed to create a new topic in the forum
  98. """
  99. return check_perm(user=user, perm='posttopic', forum=forum)
  100. def crop_title(title):
  101. """
  102. Crops the title to a specified length
  103. """
  104. length = current_app.config['TITLE_LENGTH']
  105. if len(title) > length:
  106. return title[:length] + "..."
  107. return title
  108. def render_markup(text):
  109. return render_bbcode(text)
  110. def is_online(user):
  111. return user.lastseen >= time_diff()
  112. def time_diff():
  113. now = datetime.utcnow()
  114. diff = now - timedelta(minutes=current_app.config['ONLINE_LAST_MINUTES'])
  115. return diff
  116. def format_date(value, format='%Y-%m-%d'):
  117. """
  118. Returns a formatted time string
  119. """
  120. return value.strftime(format)
  121. def time_since(value):
  122. return time_delta_format(value)
  123. def time_delta_format(dt, default=None):
  124. """
  125. Returns string representing "time since" e.g.
  126. 3 days ago, 5 hours ago etc.
  127. Ref: https://bitbucket.org/danjac/newsmeme/src/a281babb9ca3/newsmeme/
  128. """
  129. if default is None:
  130. default = 'just now'
  131. now = datetime.utcnow()
  132. diff = now - dt
  133. periods = (
  134. (diff.days / 365, 'year', 'years'),
  135. (diff.days / 30, 'month', 'months'),
  136. (diff.days / 7, 'week', 'weeks'),
  137. (diff.days, 'day', 'days'),
  138. (diff.seconds / 3600, 'hour', 'hours'),
  139. (diff.seconds / 60, 'minute', 'minutes'),
  140. (diff.seconds, 'second', 'seconds'),
  141. )
  142. for period, singular, plural in periods:
  143. if not period:
  144. continue
  145. if period == 1:
  146. return u'%d %s ago' % (period, singular)
  147. else:
  148. return u'%d %s ago' % (period, plural)
  149. return default
  150. class DenormalizedText(Mutable, types.TypeDecorator):
  151. """
  152. Stores denormalized primary keys that can be
  153. accessed as a set.
  154. :param coerce: coercion function that ensures correct
  155. type is returned
  156. :param separator: separator character
  157. Source: https://github.com/imwilsonxu/fbone/blob/master/fbone/user/models.py#L13-L45
  158. """
  159. impl = types.Text
  160. def __init__(self, coerce=int, separator=" ", **kwargs):
  161. self.coerce = coerce
  162. self.separator = separator
  163. super(DenormalizedText, self).__init__(**kwargs)
  164. def process_bind_param(self, value, dialect):
  165. if value is not None:
  166. items = [str(item).strip() for item in value]
  167. value = self.separator.join(item for item in items if item)
  168. return value
  169. def process_result_value(self, value, dialect):
  170. if not value:
  171. return set()
  172. return set(self.coerce(item) for item in value.split(self.separator))
  173. def copy_value(self, value):
  174. return set(value)
  175. class SelectDateWidget(object):
  176. """
  177. Renders a DateTime field with 3 selects.
  178. For more information see: http://stackoverflow.com/a/14664504
  179. """
  180. FORMAT_CHOICES = {
  181. '%d': [(x, str(x)) for x in range(1, 32)],
  182. '%m': [(x, str(x)) for x in range(1, 13)]
  183. }
  184. FORMAT_CLASSES = {
  185. '%d': 'select_date_day',
  186. '%m': 'select_date_month',
  187. '%Y': 'select_date_year'
  188. }
  189. def __init__(self, years=range(1930, datetime.utcnow().year+1)):
  190. super(SelectDateWidget, self).__init__()
  191. self.FORMAT_CHOICES['%Y'] = [(x, str(x)) for x in years]
  192. def __call__(self, field, **kwargs):
  193. field_id = kwargs.pop('id', field.id)
  194. html = []
  195. allowed_format = ['%d', '%m', '%Y']
  196. for format in field.format.split():
  197. if (format in allowed_format):
  198. choices = self.FORMAT_CHOICES[format]
  199. id_suffix = format.replace('%', '-')
  200. id_current = field_id + id_suffix
  201. kwargs['class'] = self.FORMAT_CLASSES[format]
  202. try:
  203. del kwargs['placeholder']
  204. except:
  205. pass
  206. html.append('<select %s>' % html_params(name=field.name,
  207. id=id_current,
  208. **kwargs))
  209. if field.data:
  210. current_value = int(field.data.strftime(format))
  211. else:
  212. current_value = None
  213. for value, label in choices:
  214. selected = (value == current_value)
  215. html.append(Select.render_option(value, label, selected))
  216. html.append('</select>')
  217. else:
  218. html.append(format)
  219. html.append(
  220. """<input type="hidden" value="{}" {}></input>""".format(
  221. html_params(name=field.name, id=id_current, **kwargs)))
  222. html.append(' ')
  223. return HTMLString(''.join(html))