helpers.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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 current_app, 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 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 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. # If there are no topics in the forum, mark it as read
  110. if forum and forum.topic_count == 0:
  111. return False
  112. # If the user hasn't visited a topic in the forum - therefore,
  113. # forumsread is None and we need to check if it is still unread
  114. if forum and not forumsread:
  115. return forum.last_post.date_created > read_cutoff
  116. # the user has visited a topic in this forum, check if there is a new post
  117. return forumsread.last_read < forum.last_post.date_created
  118. def topic_is_unread(topic, topicsread, user, forumsread=None):
  119. """Checks if a topic is unread
  120. :param topic: The topic that should be checked if it is unread
  121. :param topicsread: The topicsread object for the topic
  122. :param user: The user who should be checked if he has read the last post
  123. in the topic
  124. :param forumsread: The forumsread object in which the topic is. If you
  125. also want to check if the user has marked the forum as
  126. read, than you will also need to pass an forumsread
  127. object.
  128. """
  129. if not user.is_authenticated():
  130. return False
  131. read_cutoff = datetime.utcnow() - timedelta(
  132. days=flaskbb_config["TRACKER_LENGTH"])
  133. # topicsread is none if the user has marked the forum as read
  134. # or if he hasn't visited yet
  135. if topic and not topicsread and topic.last_post.date_created > read_cutoff:
  136. # user has cleared the forum sometime ago - check if there is a new post
  137. if forumsread and forumsread.cleared is not None:
  138. return forumsread.cleared < topic.last_post.date_created
  139. # user hasn't read the topic yet, or it has been cleared
  140. return True
  141. return topicsread.last_read < topic.last_post.date_created
  142. def mark_online(user_id, guest=False):
  143. """Marks a user as online
  144. :param user_id: The id from the user who should be marked as online
  145. :param guest: If set to True, it will add the user to the guest activity
  146. instead of the user activity.
  147. Ref: http://flask.pocoo.org/snippets/71/
  148. """
  149. now = int(time.time())
  150. expires = now + (flaskbb_config['ONLINE_LAST_MINUTES'] * 60) + 10
  151. if guest:
  152. all_users_key = 'online-guests/%d' % (now // 60)
  153. user_key = 'guest-activity/%s' % user_id
  154. else:
  155. all_users_key = 'online-users/%d' % (now // 60)
  156. user_key = 'user-activity/%s' % user_id
  157. p = redis.pipeline()
  158. p.sadd(all_users_key, user_id)
  159. p.set(user_key, now)
  160. p.expireat(all_users_key, expires)
  161. p.expireat(user_key, expires)
  162. p.execute()
  163. def get_last_user_activity(user_id, guest=False):
  164. """Returns the last active time from a given user_id
  165. :param user_id: The user id for whom you want to know the latest activity
  166. :param guest: If the user is a guest (not signed in)
  167. """
  168. if guest:
  169. last_active = redis.get('guest-activity/%s' % user_id)
  170. else:
  171. last_active = redis.get('user-activity/%s' % user_id)
  172. if last_active is None:
  173. return None
  174. return datetime.utcfromtimestamp(int(last_active))
  175. def get_online_users(guest=False):
  176. """Returns all online users within a specified time range
  177. :param guest: If True, it will return the online guests
  178. """
  179. current = int(time.time()) // 60
  180. minutes = xrange(flaskbb_config['ONLINE_LAST_MINUTES'])
  181. if guest:
  182. return redis.sunion(['online-guests/%d' % (current - x)
  183. for x in minutes])
  184. return redis.sunion(['online-users/%d' % (current - x)
  185. for x in minutes])
  186. def crop_title(title):
  187. """Crops the title to a specified length
  188. :param title: The title that should be cropped
  189. """
  190. length = flaskbb_config['TITLE_LENGTH']
  191. if len(title) > length:
  192. return title[:length] + "..."
  193. return title
  194. def render_markup(text):
  195. """Renders the given text as bbcode
  196. :param text: The text that should be rendered as bbcode
  197. """
  198. return render_bbcode(text)
  199. def is_online(user):
  200. """A simple check to see if the user was online within a specified
  201. time range
  202. :param user: The user who needs to be checked
  203. """
  204. return user.lastseen >= time_diff()
  205. def time_diff():
  206. """Calculates the time difference between now and the ONLINE_LAST_MINUTES
  207. variable from the configuration.
  208. """
  209. now = datetime.utcnow()
  210. diff = now - timedelta(minutes=flaskbb_config['ONLINE_LAST_MINUTES'])
  211. return diff
  212. def format_date(value, format='%Y-%m-%d'):
  213. """Returns a formatted time string
  214. :param value: The datetime object that should be formatted
  215. :param format: How the result should look like. A full list of available
  216. directives is here: http://goo.gl/gNxMHE
  217. """
  218. return value.strftime(format)
  219. def time_since(value):
  220. """Just a interface for `time_delta_format`"""
  221. return time_delta_format(value)
  222. def time_delta_format(dt, default=None):
  223. """Returns a string representing time since e.g. 3 days ago, 5 hours ago.
  224. ref: https://bitbucket.org/danjac/newsmeme/src/a281babb9ca3/newsmeme/
  225. note: when Babel1.0 is released, use format_timedelta/timedeltaformat
  226. instead
  227. """
  228. if default is None:
  229. default = 'just now'
  230. now = datetime.utcnow()
  231. diff = now - dt
  232. periods = (
  233. (diff.days / 365, 'year', 'years'),
  234. (diff.days / 30, 'month', 'months'),
  235. (diff.days / 7, 'week', 'weeks'),
  236. (diff.days, 'day', 'days'),
  237. (diff.seconds / 3600, 'hour', 'hours'),
  238. (diff.seconds / 60, 'minute', 'minutes'),
  239. (diff.seconds, 'second', 'seconds'),
  240. )
  241. for period, singular, plural in periods:
  242. if not period:
  243. continue
  244. if period == 1:
  245. return u'%d %s ago' % (period, singular)
  246. else:
  247. return u'%d %s ago' % (period, plural)
  248. return default