Нема описа

home.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import copy
  2. from datetime import datetime, timedelta
  3. import math
  4. from django import forms
  5. from django.contrib.auth.decorators import login_required
  6. from django.contrib.auth.models import User
  7. from django.core.mail import EmailMessage
  8. from django.core.urlresolvers import reverse
  9. from django.db import IntegrityError
  10. from django.db.models import Prefetch, Count
  11. from django.http import HttpResponseRedirect
  12. from django.shortcuts import render
  13. from django.template.loader import render_to_string
  14. from django.utils import timezone
  15. from django.utils.translation import ugettext as _, get_language
  16. import arrow
  17. from babel.dates import format_timedelta, format_datetime
  18. from graphos.renderers import gchart
  19. from graphos.sources.model import ModelDataSource
  20. from graphos.sources.simple import SimpleDataSource
  21. import numpy as np
  22. import pandas as pd
  23. from counter.models import *
  24. from counter.utils import parseSeumReason
  25. # Number of counters displayed on the home page's best seumeurs graph
  26. bestSeumeursNumber = 15
  27. @login_required
  28. def index(request):
  29. # Used later to keep track of the maximum JSS
  30. lastResets = []
  31. no_seum_delta = timedelta.max
  32. # First select our counter
  33. try:
  34. myCounter = Counter.objects.get(user__id=request.user.id)
  35. myLastReset = Reset.objects.select_related('who').filter(counter=myCounter).order_by('-timestamp').first()
  36. if myLastReset is None:
  37. # This person never had the seum
  38. myCounter.lastReset = Reset()
  39. myCounter.lastReset.delta = no_seum_delta
  40. myCounter.lastReset.formatted_delta = format_timedelta(myCounter.lastReset.delta, locale=get_language(), threshold=1)
  41. myCounter.lastReset.noSeum = True
  42. else:
  43. myCounter.lastReset = myLastReset
  44. myCounter.lastReset.noSeum = False
  45. if myCounter.lastReset.who is None or myCounter.lastReset.who.id == myCounter.id:
  46. myCounter.lastReset.selfSeum = True
  47. else:
  48. myCounter.lastReset.selfSeum = False
  49. likesMe = list(Like.objects.select_related('liker').filter(reset=myCounter.lastReset))
  50. myCounter.likeCount = len(likesMe)
  51. if myCounter.likeCount > 0:
  52. myCounter.likersString = ", ".join([like.liker.trigramme for like in likesMe])
  53. myCounter.lastReset.formatted_delta = arrow.Arrow.fromdatetime(myCounter.lastReset.timestamp).humanize(locale=get_language())
  54. except Counter.DoesNotExist:
  55. return HttpResponseRedirect(reverse('login'))
  56. # Building data for counters display
  57. counters = Counter.objects.prefetch_related(
  58. 'resets__likes',
  59. Prefetch(
  60. 'resets',
  61. queryset=Reset.objects.prefetch_related('who', Prefetch('likes', queryset=Like.objects.select_related('liker'))).order_by('-timestamp'),
  62. to_attr='lastReset'
  63. )
  64. )
  65. for counter in counters:
  66. # Only the last reset is displayed
  67. lastReset = list(counter.lastReset)
  68. if len(lastReset) == 0: # This person never had the seum
  69. counter.lastReset = Reset()
  70. counter.lastReset.delta = no_seum_delta
  71. counter.lastReset.formatted_delta = format_timedelta(counter.lastReset.delta, locale=get_language(), threshold=1)
  72. counter.lastReset.noSeum = True
  73. counter.lastReset.likes_count = -1
  74. counter.CSSclass = "warning"
  75. else: # This person already had the seum
  76. counter.lastReset = lastReset[0]
  77. # To display the last seum we have to know if it is self-inflicted
  78. if counter.lastReset.who is None or counter.lastReset.who == counter:
  79. counter.lastReset.selfSeum = True
  80. else:
  81. counter.lastReset.selfSeum = False
  82. # Now we compute the duration since the reset
  83. counter.lastReset.noSeum = False
  84. counter.lastReset.delta = datetime.now() - counter.lastReset.timestamp.replace(tzinfo=None)
  85. # Defining CSS attributes for the counter
  86. counter.CSSclass = 'primary' if counter == myCounter else 'default'
  87. # Computing the total number of likes for this counter
  88. likesMe = list(counter.lastReset.likes.all())
  89. counter.lastReset.likes_count = len(likesMe)
  90. counter.alreadyLiked = myCounter.id in [likeMe.liker.id for likeMe in likesMe]
  91. if counter.lastReset.likes_count > 0:
  92. counter.likersString = ", ".join([like.liker.trigramme for like in likesMe])
  93. counter.lastReset.formatted_delta = arrow.Arrow.fromdatetime(counter.lastReset.timestamp).humanize(locale=get_language())
  94. counter.likeCount = counter.lastReset.likes_count
  95. counter.isHidden = 'hidden'
  96. if myCounter.sort_by_score:
  97. # Now we sort the counters according to a reddit-like ranking formula
  98. # We take into account the number of likes of a reset and recentness
  99. # The log on the score will give increased value to the first likes
  100. # The counters with no seum have a like count of -1 by convention
  101. sorting_key = lambda t: - (math.log(t.lastReset.likes_count + 2) / (1 + (t.lastReset.delta.total_seconds()) / (24 * 3600)))
  102. counters = sorted(counters, key=sorting_key)
  103. else:
  104. counters = sorted(counters, key=lambda t: + t.lastReset.delta.total_seconds())
  105. # ### GRAPHS ###
  106. resets_raw = list(Reset.objects.select_related('who', 'counter').annotate(likes_count=Count('likes')))
  107. likes_raw = list(Like.objects.select_related('liker', 'reset__counter').all())
  108. hashtags_raw = list(Hashtag.objects.select_related('keyword').all())
  109. # Prepare pandas.DataFrames to efficiently process the data
  110. # About the counters
  111. resets_cols = ['date', 'counter', 'counter_trigram', 'who', 'who_trigram', 'reason', 'likes_count']
  112. resets_data = [[r.timestamp, r.counter.id, r.counter.trigramme, r.who, r.who, r.reason, r.likes_count] for r in resets_raw]
  113. for r in resets_data:
  114. r[3] = 0 if r[3] is None else r[3].id
  115. r[4] = '' if r[4] is None else r[4].trigramme
  116. resets_df = pd.DataFrame(resets_data, columns=resets_cols)
  117. resets_df['timestamp'] = resets_df.date.map(lambda d: d.timestamp())
  118. resets_df['self_seum'] = (resets_df.who.eq(np.zeros(resets_df.shape[0])) | resets_df.who.eq(resets_df.counter)).map(float)
  119. resets_df['formatted_delta'] = resets_df.date.map(lambda d: arrow.Arrow.fromdatetime(d).humanize(locale=get_language()))
  120. # About the likes
  121. likes_cols = ['liker', 'liker_trigram', 'counter', 'counter_trigram']
  122. likes_data = [[l.liker.id, l.liker.trigramme, l.reset.counter.id, l.reset.counter.trigramme] for l in likes_raw]
  123. likes_df = pd.DataFrame(likes_data, columns=likes_cols)
  124. # About the hashtags
  125. hashtags_cols = ['keyword']
  126. hashtags_data = [[h.keyword.text] for h in hashtags_raw]
  127. hashtags_df = pd.DataFrame(hashtags_data, columns=hashtags_cols)
  128. # Timeline graph
  129. timeline_resets = resets_df[resets_df.date > (datetime.now() - timedelta(days=1))].copy().reset_index()
  130. if timeline_resets.shape[0] == 0:
  131. noTimeline = True
  132. line_chart = None
  133. else:
  134. noTimeline = False
  135. # Construct legend for timeline dots
  136. legend_ = np.zeros(timeline_resets.shape[0], dtype=np.object)
  137. for i in range(timeline_resets.shape[0]):
  138. row = timeline_resets.iloc[i]
  139. if row['self_seum'] == 1:
  140. legend_[i] = _('%(counter)s: %(reason)s') % {'counter': row['counter_trigram'], 'reason': row['reason']}
  141. else:
  142. legend_[i] = _('%(who)s to %(counter)s: %(reason)s') % {'who': row['who_trigram'], 'counter': row['counter_trigram'], 'reason': row['reason']}
  143. timeline_resets['legend'] = legend_
  144. # Generate graph
  145. resets_ = [['', _('Seum')]]
  146. for i in range(timeline_resets.shape[0]):
  147. r = timeline_resets.iloc[i]
  148. resets_.append([{'v': r.timestamp, 'f': r.formatted_delta}, {'v': 0, 'f': r.legend}])
  149. # resets_.append({
  150. # 'timestamp': {'v': r.date.timestamp(), 'f': r.formatted_delta},
  151. # 'Seum': {'v': 0, 'f': r.legend},
  152. # })
  153. line_data = SimpleDataSource(resets_)
  154. line_chart = gchart.LineChart(line_data, options={
  155. 'lineWidth': 0,
  156. 'pointSize': 10,
  157. 'title': '',
  158. 'vAxis': {'ticks': []},
  159. 'hAxis': {
  160. 'ticks': [
  161. {'v': (datetime.now() - timedelta(days=1)
  162. ).timestamp(), 'f': _('24h ago')},
  163. {'v': datetime.now().timestamp(), 'f': _('Now')}
  164. ]
  165. },
  166. 'legend': 'none',
  167. 'height': 90
  168. })
  169. # Graph of greatest seumers
  170. seum_counts_df = resets_df[['counter_trigram', 'self_seum']].copy()
  171. seum_counts_df['seum_count'] = np.ones(seum_counts_df.shape[0], dtype=np.float32)
  172. seum_counts_df = seum_counts_df.groupby(['counter_trigram']).sum().reset_index()
  173. # TODO: Add the ratio self_seum / seum_count
  174. if (seum_counts_df.shape[0] == 0):
  175. noBestSeum = True
  176. best_chart = None
  177. else:
  178. noBestSeum = False
  179. seum_counts_data = seum_counts_df.sort_values(by='seum_count', ascending=False)[['counter_trigram', 'seum_count']].values.tolist()
  180. seum_counts_data.insert(0, [_('Trigram'), _('Number of seums')])
  181. best_data = SimpleDataSource(seum_counts_data[:bestSeumeursNumber])
  182. best_chart = gchart.ColumnChart(best_data, options={
  183. 'title': '',
  184. 'legend': 'none',
  185. 'vAxis': {'title': _('Number of seums')},
  186. 'hAxis': {'title': _('Trigram')},
  187. })
  188. # Graph of seum activity
  189. resets_act = resets_df[resets_df.date > (timezone.now() - timedelta(days=365))][['date']].copy()
  190. resets_act['year'] = resets_df.date.map(lambda d: d.year)
  191. resets_act['month'] = resets_df.date.map(lambda d: d.month)
  192. resets_act = resets_act.drop(['date'], axis=1)
  193. resets_act['month_counts'] = np.ones(resets_act.shape[0], dtype=int)
  194. resets_act = resets_act.groupby(['year', 'month']).sum().reset_index()
  195. if resets_act.shape[0] == 0:
  196. noSeumActivity = True
  197. activity_chart = None
  198. else:
  199. noSeumActivity = False
  200. seumActivity = [
  201. [arrow.Arrow(a[0], a[1], 1).format("MMM YYYY", locale=get_language()).capitalize(), a[2]]
  202. for a in resets_act.values.tolist()
  203. ]
  204. seumActivity.insert(0, [_('Month'), _('Number of seums')])
  205. activity_data = SimpleDataSource(seumActivity)
  206. activity_chart = gchart.ColumnChart(activity_data, options={
  207. 'title': '',
  208. 'legend': 'none',
  209. 'vAxis': {'title': _('Number of seums')},
  210. 'hAxis': {'title': _('Month')},
  211. })
  212. # Graph of best likers
  213. best_likers_df = likes_df.drop(['liker', 'counter', 'counter_trigram'], axis=1)
  214. best_likers_df['count'] = np.ones(best_likers_df.shape[0], dtype=int)
  215. best_likers_df = best_likers_df.groupby(['liker_trigram']).sum().reset_index()
  216. if best_likers_df.shape[0] == 0:
  217. noBestLikers = True
  218. likers_chart = None
  219. else:
  220. noBestLikers = False
  221. likersCounts = best_likers_df.sort_values(by='count', ascending=False).values.tolist()
  222. likersCounts.insert(0, [_('Trigram'), _('Number of given likes')])
  223. likers_data = SimpleDataSource(likersCounts[:bestSeumeursNumber])
  224. likers_chart = gchart.ColumnChart(likers_data, options={
  225. 'title': '',
  226. 'legend': 'none',
  227. 'vAxis': {'title': _('Number of given likes')},
  228. 'hAxis': {'title': _('Trigram')},
  229. })
  230. # Graph of popular hashtags
  231. hashtags_df['count'] = np.ones(hashtags_df.shape[0], dtype=int)
  232. hashtags_df = hashtags_df.groupby(['keyword']).sum().reset_index()
  233. hashtags_df['keyword'] = hashtags_df.keyword.map(lambda x: '#' + x)
  234. if hashtags_df.shape[0] == 0:
  235. noBestHashtags = True
  236. hashtags_chart = None
  237. else:
  238. noBestHashtags = False
  239. hashtags_data = hashtags_df.sort_values(by='count', ascending=False).values.tolist()
  240. hashtags_data.insert(0, [_('Hashtag'), _('Number of seums containing the hashtag')])
  241. hashtags_data = SimpleDataSource(hashtags_data[:bestSeumeursNumber])
  242. hashtags_chart = gchart.ColumnChart(hashtags_data, options={
  243. 'title': '',
  244. 'legend': 'none',
  245. 'vAxis': {'title': _('Number of seums containing the hashtag')},
  246. 'hAxis': {'title': _('Hashtag')},
  247. })
  248. # Graph of best likee
  249. best_likees_df = likes_df.drop(['counter', 'liker', 'liker_trigram'], axis=1)
  250. best_likees_df['count'] = np.ones(best_likees_df.shape[0], dtype=int)
  251. best_likees_df = best_likees_df.groupby(['counter_trigram']).sum().reset_index()
  252. if best_likees_df.shape[0] == 0:
  253. noBestLikees = True
  254. likees_chart = None
  255. else:
  256. noBestLikees = False
  257. likeesCounts = best_likees_df.sort_values(by='count', ascending=False).values.tolist()
  258. likeesCounts.insert(0, [_('Trigram'), _('Number of received likes')])
  259. likees_data = SimpleDataSource(likeesCounts[:bestSeumeursNumber])
  260. likees_chart = gchart.ColumnChart(likees_data, options={
  261. 'title': '',
  262. 'legend': 'none',
  263. 'vAxis': {'title': _('Number of received likes')},
  264. 'hAxis': {'title': _('Trigram')},
  265. })
  266. # At last we render the page
  267. return render(request, 'homeTemplate.html', {
  268. 'counters': counters,
  269. 'line_chart': line_chart,
  270. 'best_chart': best_chart,
  271. 'likers_chart': likers_chart,
  272. 'likees_chart': likees_chart,
  273. 'hashtags_chart': hashtags_chart,
  274. 'activity_chart': activity_chart,
  275. 'noTimeline': noTimeline,
  276. 'noBestSeum': noBestSeum,
  277. 'noBestLikers': noBestLikers,
  278. 'noBestLikees': noBestLikees,
  279. 'noBestHashtags': noBestHashtags,
  280. 'noSeumActivity': noSeumActivity,
  281. 'myCounter': myCounter,
  282. })
  283. @login_required
  284. def toggleEmailNotifications(request):
  285. counter = Counter.objects.get(user=request.user)
  286. counter.email_notifications = not counter.email_notifications
  287. counter.save()
  288. return HttpResponseRedirect(reverse('home'))
  289. @login_required
  290. def toggleScoreSorting(request):
  291. counter = Counter.objects.get(user=request.user)
  292. counter.sort_by_score = not counter.sort_by_score
  293. counter.save()
  294. return HttpResponseRedirect(reverse('home'))