helpers.py 12 KB

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