helpers.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils.helpers
  4. ~~~~~~~~~~~~~~~~~~~~
  5. A few helpers that are used by flaskbb
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import re
  10. import time
  11. import itertools
  12. import operator
  13. from unicodedata import normalize
  14. from datetime import datetime, timedelta
  15. from flask import session
  16. from flask.ext.themes2 import render_theme_template
  17. from flask.ext.login import current_user
  18. from postmarkup import render_bbcode
  19. from flaskbb.extensions import redis
  20. from flaskbb.utils.settings import flaskbb_config
  21. _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
  22. def slugify(text, delim=u'-'):
  23. """Generates an slightly worse ASCII-only slug.
  24. Taken from the Flask Snippets page.
  25. :param text: The text which should be slugified
  26. :param delim: Default "-". The delimeter for whitespace
  27. """
  28. result = []
  29. for word in _punct_re.split(text.lower()):
  30. word = normalize('NFKD', word).encode('ascii', 'ignore')
  31. if word:
  32. result.append(word)
  33. return unicode(delim.join(result))
  34. def render_template(template, **context):
  35. """A helper function that uses the `render_theme_template` function
  36. without needing to edit all the views
  37. """
  38. if current_user.is_authenticated() and current_user.theme:
  39. theme = current_user.theme
  40. else:
  41. theme = session.get('theme', flaskbb_config['DEFAULT_THEME'])
  42. return render_theme_template(theme, template, **context)
  43. def get_categories_and_forums(query_result, user):
  44. """Returns a list with categories. Every category has a list for all
  45. their associated forums.
  46. The structure looks like this::
  47. [(<Category 1>,
  48. [(<Forum 1>, None),
  49. (<Forum 2>, <flaskbb.forum.models.ForumsRead at 0x38fdb50>)]),
  50. (<Category 2>,
  51. [(<Forum 3>, None),
  52. (<Forum 4>, None)])]
  53. and to unpack the values you can do this::
  54. In [110]: for category, forums in x:
  55. .....: print category
  56. .....: for forum, forumsread in forums:
  57. .....: print "\t", forum, forumsread
  58. This will print something this:
  59. <Category 1>
  60. <Forum 1> None
  61. <Forum 2> <flaskbb.forum.models.ForumsRead object at 0x38fdb50>
  62. <Category 2>
  63. <Forum 3> None
  64. <Forum 4> None
  65. :param query_result: A tuple (KeyedTuple) with all categories and forums
  66. :param user: The user object is needed because a signed out user does not
  67. have the ForumsRead relation joined.
  68. """
  69. it = itertools.groupby(query_result, operator.itemgetter(0))
  70. forums = []
  71. if user.is_authenticated():
  72. for key, value in it:
  73. forums.append((key, [(item[1], item[2]) for item in value]))
  74. else:
  75. for key, value in it:
  76. forums.append((key, [(item[1], None) for item in value]))
  77. return forums
  78. def get_forums(query_result, user):
  79. """Returns a tuple which contains the category and the forums as list.
  80. This is the counterpart for get_categories_and_forums and especially
  81. usefull when you just need the forums for one category.
  82. For example::
  83. (<Category 2>,
  84. [(<Forum 3>, None),
  85. (<Forum 4>, None)])
  86. :param query_result: A tuple (KeyedTuple) with all categories and forums
  87. :param user: The user object is needed because a signed out user does not
  88. have the ForumsRead relation joined.
  89. """
  90. it = itertools.groupby(query_result, operator.itemgetter(0))
  91. if user.is_authenticated():
  92. for key, value in it:
  93. forums = key, [(item[1], item[2]) for item in value]
  94. else:
  95. for key, value in it:
  96. forums = key, [(item[1], None) for item in value]
  97. return forums
  98. def forum_is_unread(forum, forumsread, user):
  99. """Checks if a forum is unread
  100. :param forum: The forum that should be checked if it is unread
  101. :param forumsread: The forumsread object for the forum
  102. :param user: The user who should be checked if he has read the forum
  103. """
  104. # If the user is not signed in, every forum is marked as read
  105. if not user.is_authenticated():
  106. return False
  107. read_cutoff = datetime.utcnow() - timedelta(
  108. days=flaskbb_config["TRACKER_LENGTH"])
  109. # disable tracker if read_cutoff is set to 0
  110. if read_cutoff == 0:
  111. return False
  112. # If there are no topics in the forum, mark it as read
  113. if forum and forum.topic_count == 0:
  114. return False
  115. # If the user hasn't visited a topic in the forum - therefore,
  116. # forumsread is None and we need to check if it is still unread
  117. if forum and not forumsread:
  118. return forum.last_post.date_created > read_cutoff
  119. try:
  120. return forumsread.cleared > forum.last_post.date_created
  121. except TypeError:
  122. return forumsread.last_read < forum.last_post.date_created
  123. def topic_is_unread(topic, topicsread, user, forumsread=None):
  124. """Checks if a topic is unread.
  125. :param topic: The topic that should be checked if it is unread
  126. :param topicsread: The topicsread object for the topic
  127. :param user: The user who should be checked if he has read the last post
  128. in the topic
  129. :param forumsread: The forumsread object in which the topic is. If you
  130. also want to check if the user has marked the forum as
  131. read, than you will also need to pass an forumsread
  132. object.
  133. """
  134. if not user.is_authenticated():
  135. return False
  136. read_cutoff = datetime.utcnow() - timedelta(
  137. days=flaskbb_config["TRACKER_LENGTH"])
  138. # disable tracker if read_cutoff is set to 0
  139. if read_cutoff == 0:
  140. return False
  141. # check read_cutoff
  142. if topic.last_post.date_created < read_cutoff:
  143. return False
  144. # topicsread is none if the user has marked the forum as read
  145. # or if he hasn't visited yet
  146. if topicsread is None:
  147. # user has cleared the forum sometime ago - check if there is a new post
  148. if forumsread and forumsread.cleared is not None:
  149. return forumsread.cleared < topic.last_post.date_created
  150. # user hasn't read the topic yet, or there is a new post since the user
  151. # has marked the forum as read
  152. return True
  153. # check if there is a new post since the user's last topic visit
  154. return topicsread.last_read < topic.last_post.date_created
  155. def mark_online(user_id, guest=False):
  156. """Marks a user as online
  157. :param user_id: The id from the user who should be marked as online
  158. :param guest: If set to True, it will add the user to the guest activity
  159. instead of the user activity.
  160. Ref: http://flask.pocoo.org/snippets/71/
  161. """
  162. now = int(time.time())
  163. expires = now + (flaskbb_config['ONLINE_LAST_MINUTES'] * 60) + 10
  164. if guest:
  165. all_users_key = 'online-guests/%d' % (now // 60)
  166. user_key = 'guest-activity/%s' % user_id
  167. else:
  168. all_users_key = 'online-users/%d' % (now // 60)
  169. user_key = 'user-activity/%s' % user_id
  170. p = redis.pipeline()
  171. p.sadd(all_users_key, user_id)
  172. p.set(user_key, now)
  173. p.expireat(all_users_key, expires)
  174. p.expireat(user_key, expires)
  175. p.execute()
  176. def get_last_user_activity(user_id, guest=False):
  177. """Returns the last active time from a given user_id
  178. :param user_id: The user id for whom you want to know the latest activity
  179. :param guest: If the user is a guest (not signed in)
  180. """
  181. if guest:
  182. last_active = redis.get('guest-activity/%s' % user_id)
  183. else:
  184. last_active = redis.get('user-activity/%s' % user_id)
  185. if last_active is None:
  186. return None
  187. return datetime.utcfromtimestamp(int(last_active))
  188. def get_online_users(guest=False):
  189. """Returns all online users within a specified time range
  190. :param guest: If True, it will return the online guests
  191. """
  192. current = int(time.time()) // 60
  193. minutes = xrange(flaskbb_config['ONLINE_LAST_MINUTES'])
  194. if guest:
  195. return redis.sunion(['online-guests/%d' % (current - x)
  196. for x in minutes])
  197. return redis.sunion(['online-users/%d' % (current - x)
  198. for x in minutes])
  199. def crop_title(title):
  200. """Crops the title to a specified length
  201. :param title: The title that should be cropped
  202. """
  203. length = flaskbb_config['TITLE_LENGTH']
  204. if len(title) > length:
  205. return title[:length] + "..."
  206. return title
  207. def render_markup(text):
  208. """Renders the given text as bbcode
  209. :param text: The text that should be rendered as bbcode
  210. """
  211. return render_bbcode(text)
  212. def is_online(user):
  213. """A simple check to see if the user was online within a specified
  214. time range
  215. :param user: The user who needs to be checked
  216. """
  217. return user.lastseen >= time_diff()
  218. def time_diff():
  219. """Calculates the time difference between now and the ONLINE_LAST_MINUTES
  220. variable from the configuration.
  221. """
  222. now = datetime.utcnow()
  223. diff = now - timedelta(minutes=flaskbb_config['ONLINE_LAST_MINUTES'])
  224. return diff
  225. def format_date(value, format='%Y-%m-%d'):
  226. """Returns a formatted time string
  227. :param value: The datetime object that should be formatted
  228. :param format: How the result should look like. A full list of available
  229. directives is here: http://goo.gl/gNxMHE
  230. """
  231. return value.strftime(format)
  232. def time_since(value):
  233. """Just a interface for `time_delta_format`"""
  234. return time_delta_format(value)
  235. def time_delta_format(dt, default=None):
  236. """Returns a string representing time since e.g. 3 days ago, 5 hours ago.
  237. ref: https://bitbucket.org/danjac/newsmeme/src/a281babb9ca3/newsmeme/
  238. note: when Babel1.0 is released, use format_timedelta/timedeltaformat
  239. instead
  240. """
  241. if default is None:
  242. default = 'just now'
  243. now = datetime.utcnow()
  244. diff = now - dt
  245. periods = (
  246. (diff.days / 365, 'year', 'years'),
  247. (diff.days / 30, 'month', 'months'),
  248. (diff.days / 7, 'week', 'weeks'),
  249. (diff.days, 'day', 'days'),
  250. (diff.seconds / 3600, 'hour', 'hours'),
  251. (diff.seconds / 60, 'minute', 'minutes'),
  252. (diff.seconds, 'second', 'seconds'),
  253. )
  254. for period, singular, plural in periods:
  255. if not period:
  256. continue
  257. if period == 1:
  258. return u'%d %s ago' % (period, singular)
  259. else:
  260. return u'%d %s ago' % (period, plural)
  261. return default