Brak opisu

home.py 14KB

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