Нет описания

views.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. from django.shortcuts import render
  2. from counter.models import Counter, Reset, Like, Keyword, Hashtag
  3. from django.contrib.auth.models import User
  4. from babel.dates import format_timedelta, format_datetime
  5. from datetime import datetime, timedelta
  6. from django import forms
  7. from django.http import HttpResponseRedirect
  8. from django.core.mail import EmailMessage
  9. from django.contrib.auth.decorators import login_required
  10. from django.core.urlresolvers import reverse
  11. from django.db import IntegrityError
  12. from graphos.renderers import gchart
  13. from django.template.loader import render_to_string
  14. from graphos.sources.simple import SimpleDataSource
  15. from graphos.sources.model import ModelDataSource
  16. import random
  17. import math
  18. import copy
  19. import functools
  20. from django.utils import timezone
  21. from counter.utils import parseSeumReason
  22. # JSS above this limit will not be displayed on the home page col graph
  23. JSS_limit = 7
  24. # Number of counters displayed on the home page's best seumeurs graph
  25. bestSeumeursNumber = 15
  26. @login_required
  27. def home(request):
  28. # Used later to keep track of the maximum JSS
  29. maxJSS = 0
  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. lastReset = Reset.objects.filter(
  36. counter=myCounter).order_by('-timestamp')
  37. if (lastReset.count() == 0):
  38. # This person never had the seum
  39. myCounter.lastReset = Reset()
  40. myCounter.lastReset.delta = no_seum_delta
  41. myCounter.lastReset.noSeum = True
  42. else:
  43. myCounter.lastReset = lastReset[0]
  44. myCounter.lastReset.noSeum = False
  45. if (myCounter.lastReset.who is None or
  46. myCounter.lastReset.who.id == myCounter.id):
  47. myCounter.lastReset.selfSeum = True
  48. else:
  49. myCounter.lastReset.selfSeum = False
  50. myCounter.lastReset.delta = datetime.now(
  51. ) - myCounter.lastReset.timestamp.replace(tzinfo=None)
  52. likesMe = Like.objects.filter(
  53. reset=myCounter.lastReset)
  54. myCounter.likeCount = likesMe.count()
  55. if myCounter.likeCount:
  56. myCounter.likersString = functools.reduce(
  57. lambda a, b: a + ", " + b,
  58. [like.liker.trigramme for like in likesMe])
  59. myCounter.lastReset.formatted_delta = format_timedelta(
  60. myCounter.lastReset.delta, locale='fr', threshold=1)
  61. except Counter.DoesNotExist:
  62. return HttpResponseRedirect(reverse('login'))
  63. # Building data for counters display
  64. counters = Counter.objects.all()
  65. for counter in counters:
  66. # Only the last reset is displayed
  67. lastReset = Reset.objects.filter(
  68. counter=counter).order_by('-timestamp')
  69. if (lastReset.count() == 0): # This person never had the seum
  70. counter.lastReset = Reset()
  71. counter.lastReset.delta = no_seum_delta
  72. counter.lastReset.noSeum = True
  73. counter.CSSclass = "warning"
  74. counter.likeCount = -1
  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
  79. counter.lastReset.who.id == counter.id):
  80. counter.lastReset.selfSeum = True
  81. else:
  82. counter.lastReset.selfSeum = False
  83. # Now we compute the duration since the reset
  84. counter.lastReset.noSeum = False
  85. counter.lastReset.delta = datetime.now(
  86. ) - counter.lastReset.timestamp.replace(tzinfo=None)
  87. if ((counter.lastReset.delta.total_seconds()) / (24 * 3600) <
  88. JSS_limit):
  89. # Less than 7 JSS -> display on graph
  90. lastResets.append(
  91. [counter.trigramme,
  92. {'v': (counter.lastReset.delta.total_seconds()) /
  93. (24 * 3600),
  94. 'f': str(round(
  95. (counter.lastReset.delta.total_seconds()) /
  96. (24 * 3600), 1))}])
  97. # Updating the max JSS displayed on the graph to compute scale
  98. if (counter.lastReset.delta.total_seconds() / (24 * 3600) >
  99. maxJSS):
  100. maxJSS = (counter.lastReset.delta.total_seconds() /
  101. (24 * 3600))
  102. # Defining CSS attributes for the counter
  103. if counter.id == myCounter.id:
  104. counter.CSSclass = 'primary'
  105. else:
  106. counter.CSSclass = 'default'
  107. # Computing the total number of likes for this counter
  108. likesMe = Like.objects.filter(
  109. reset=counter.lastReset)
  110. counter.likeCount = likesMe.count()
  111. counter.alreadyLiked = (Like.objects.filter(
  112. reset=counter.lastReset, liker=myCounter).exists())
  113. if counter.likeCount > 0:
  114. counter.likersString = functools.reduce(
  115. lambda a, b: a + ", " + b,
  116. [like.liker.trigramme for like in likesMe])
  117. counter.lastReset.formatted_delta = format_timedelta(
  118. counter.lastReset.delta, locale='fr', threshold=1)
  119. counter.isHidden = 'hidden'
  120. if myCounter.sort_by_score:
  121. # Now we sort the counters according to a reddit-like ranking formula
  122. # We take into account the number of likes of a reset and its recentness
  123. # The log on the score will give increased value to the first likes
  124. # The negative exp for the time with a characteristic time of 1 day will
  125. # cause that after 1 day the « recentness score drops from 1 to 0.36
  126. # The counters with no seum have a like count of -1 by convention
  127. counters = sorted(counters, key=lambda t: - (
  128. math.log(t.likeCount + 2) /
  129. (1 + (t.lastReset.delta.total_seconds()) /
  130. (24 * 3600))))
  131. else:
  132. counters = sorted(counters, key=lambda t: +
  133. t.lastReset.delta.total_seconds())
  134. # Column graph
  135. if (len(lastResets) == 0):
  136. noGraph = True
  137. col_chart = None
  138. else:
  139. noGraph = False
  140. lastResets.sort(key=lambda x: x[1]['v'])
  141. lastResets.insert(0, ['Trigramme', 'Jours sans seum'])
  142. col_data = SimpleDataSource(lastResets)
  143. col_chart = gchart.ColumnChart(col_data, options={
  144. 'title': '',
  145. 'legend': 'none',
  146. 'vAxis': {
  147. 'viewWindow': {
  148. 'max': max(maxJSS, 1),
  149. 'min': 0
  150. },
  151. 'ticks': [1, 2, 3, 4, 5, 6, 7],
  152. 'title': 'Jours sans seum'
  153. },
  154. 'hAxis': {'title': 'Trigramme'},
  155. })
  156. # Timeline graph
  157. resets = Reset.objects.filter(
  158. timestamp__gte=timezone.now() - timedelta(days=1))
  159. if (resets.count() == 0):
  160. noTimeline = True
  161. line_chart = None
  162. else:
  163. noTimeline = False
  164. for reset in resets:
  165. reset.timestamp = {
  166. 'v': reset.timestamp.timestamp(),
  167. 'f': "Il y a " + format_timedelta(datetime.now() -
  168. reset.timestamp.replace(
  169. tzinfo=None),
  170. locale='fr', threshold=1)
  171. }
  172. if (reset.who is None or
  173. reset.who.id == reset.counter.id):
  174. reset.Seum = {'v': 0,
  175. 'f': reset.counter.trigramme +
  176. " : " + reset.reason}
  177. else:
  178. reset.Seum = {'v': 0,
  179. 'f': reset.who.trigramme + ' à ' +
  180. reset.counter.trigramme +
  181. " : " + reset.reason}
  182. line_data = ModelDataSource(resets, fields=['timestamp', 'Seum'])
  183. line_chart = gchart.LineChart(line_data, options={
  184. 'lineWidth': 0,
  185. 'pointSize': 10,
  186. 'title': '',
  187. 'vAxis': {'ticks': []},
  188. 'hAxis': {
  189. 'ticks': [
  190. {'v': (datetime.now() - timedelta(days=1)
  191. ).timestamp(), 'f': 'Il y a 24 h'},
  192. {'v': datetime.now().timestamp(), 'f': 'Présent'}
  193. ]
  194. },
  195. 'legend': 'none',
  196. 'height': 90
  197. })
  198. # Graph of greatest seumers
  199. seumCounts = []
  200. for counter in counters:
  201. seumCounts.append([counter.trigramme, Reset.objects.filter(
  202. counter=counter).count()])
  203. if (len(seumCounts) == 0):
  204. noBestSeum = True
  205. best_chart = None
  206. else:
  207. seumCounts.sort(key=lambda x: -x[1])
  208. noBestSeum = False
  209. seumCounts.insert(0, ['Trigramme', 'Nombre de seums'])
  210. best_data = SimpleDataSource(seumCounts[:bestSeumeursNumber])
  211. best_chart = gchart.ColumnChart(best_data, options={
  212. 'title': '',
  213. 'legend': 'none',
  214. 'vAxis': {'title': 'Nombre de seums'},
  215. 'hAxis': {'title': 'Trigramme'},
  216. })
  217. # Graph of seum activity
  218. resets = Reset.objects.filter(
  219. timestamp__gte=timezone.now() - timedelta(days=365))
  220. months = {}
  221. for reset in resets:
  222. monthDate = datetime(reset.timestamp.year, reset.timestamp.month, 1)
  223. months[monthDate] = months.get(monthDate, 0) + 1
  224. monthList = sorted(months.items(), key=lambda t: t[0])
  225. seumActivity = []
  226. for month in monthList:
  227. seumActivity.append(
  228. [format_datetime(month[0], locale='fr',
  229. format="MMM Y").capitalize(), month[1]])
  230. if (len(seumActivity) == 0):
  231. noSeumActivity = True
  232. activity_chart = None
  233. else:
  234. noSeumActivity = False
  235. seumActivity.insert(0, ['Mois', 'Nombre de seums'])
  236. activity_data = SimpleDataSource(seumActivity)
  237. activity_chart = gchart.ColumnChart(activity_data, options={
  238. 'title': '',
  239. 'legend': 'none',
  240. 'vAxis': {'title': 'Nombre de seums'},
  241. 'hAxis': {'title': 'Mois'},
  242. })
  243. # At last we render the page
  244. return render(request, 'homeTemplate.html', {
  245. 'counters': counters,
  246. 'col_chart': col_chart,
  247. 'line_chart': line_chart,
  248. 'best_chart': best_chart,
  249. 'activity_chart': activity_chart,
  250. 'noTimeline': noTimeline,
  251. 'noGraph': noGraph,
  252. 'noBestSeum': noBestSeum,
  253. 'noSeumActivity': noSeumActivity,
  254. 'myCounter': myCounter,
  255. })
  256. @login_required
  257. def resetCounter(request):
  258. # Update Form counter
  259. if (request.method == 'POST'):
  260. # create a form instance and populate it with data from the request:
  261. data = dict(request.POST)
  262. who = Counter.objects.get(pk=int(data['who'][0]))
  263. reason = data['reason'][0]
  264. if 'counter' in data.keys():
  265. counter = Counter.objects.get(pk=int(data['counter'][0]))
  266. else:
  267. try:
  268. counter = Counter.objects.get(trigramme=data['trigramme'][0])
  269. except Counter.DoesNotExist:
  270. return HttpResponseRedirect(data['redirect'][0])
  271. reset = Reset()
  272. reset.counter = counter
  273. reset.who = who
  274. reset.reason = data['reason'][0]
  275. reset.timestamp = datetime.now()
  276. # we check that the seumer is the autenticated user
  277. if (reset.who.user is None or
  278. reset.who.user.id != request.user.id):
  279. return HttpResponseRedirect(data['redirect'][0])
  280. reset.save()
  281. # Now we deal with the hashtags
  282. keywords = parseSeumReason(reason)
  283. for keyword in keywords:
  284. hashtag = Hashtag(reset=reset, keyword=keyword)
  285. hashtag.save()
  286. # We send the emails only to those who want
  287. emails = [u.email for u in Counter.objects.all()
  288. if u.email_notifications]
  289. # Now send emails to everyone
  290. if (reset.who is None or
  291. reset.who.id == counter.id):
  292. selfSeum = True
  293. else:
  294. selfSeum = False
  295. text_of_email = render_to_string(
  296. 'seumEmail.txt', {'reason': data['reason'][0],
  297. 'name': counter.name,
  298. 'who': reset.who,
  299. 'selfSeum': selfSeum,
  300. })
  301. email_to_send = EmailMessage(
  302. '[SeumBook] ' + counter.trigramme + ' a le seum',
  303. text_of_email,
  304. 'SeumMan <seum@merigoux.ovh>', emails, [],
  305. reply_to=emails)
  306. email_to_send.send(fail_silently=True)
  307. return HttpResponseRedirect(data['redirect'][0])
  308. @login_required
  309. def counter(request, id_counter):
  310. try:
  311. myCounter = Counter.objects.get(user__id=request.user.id)
  312. except Counter.DoesNotExist:
  313. return HttpResponseRedirect(reverse('login'))
  314. counter = Counter.objects.get(pk=id_counter)
  315. resets = Reset.objects.filter(counter=counter).order_by('-timestamp')
  316. timezero = timedelta(0)
  317. # Display
  318. if (resets.count() == 0):
  319. counter.lastReset = Reset()
  320. counter.lastReset.delta = timezero
  321. counter.lastReset.noSeum = True
  322. seumFrequency = 'inconnu'
  323. else:
  324. firstReset = copy.copy(resets[len(resets) - 1])
  325. counter.lastReset = resets[0]
  326. counter.lastReset.noSeum = False
  327. if (counter.lastReset.who is None or
  328. counter.lastReset.who.id == counter.id):
  329. counter.lastReset.selfSeum = True
  330. else:
  331. counter.lastReset.selfSeum = False
  332. counter.lastReset.delta = datetime.now(
  333. ) - counter.lastReset.timestamp.replace(tzinfo=None)
  334. counter.lastReset.formatted_delta = format_timedelta(
  335. counter.lastReset.delta, locale='fr', threshold=1)
  336. counter.seumCount = Reset.objects.filter(
  337. counter=counter).count()
  338. seumFrequency = format_timedelta((
  339. datetime.now() - firstReset.timestamp.replace(tzinfo=None)) /
  340. counter.seumCount, locale='fr', threshold=1)
  341. counter.alreadyLiked = (Like.objects.filter(
  342. reset=counter.lastReset, liker=myCounter).exists())
  343. likesMe = Like.objects.filter(
  344. reset=counter.lastReset)
  345. counter.likeCount = likesMe.count()
  346. if counter.likeCount > 0:
  347. counter.likersString = functools.reduce(
  348. lambda a, b: a + ", " + b,
  349. [like.liker.trigramme for like in likesMe])
  350. for reset in resets:
  351. if (reset.who is None or
  352. reset.who.id == reset.counter.id):
  353. reset.selfSeum = True
  354. else:
  355. reset.selfSeum = False
  356. reset.date = format_datetime(
  357. reset.timestamp, locale='fr',
  358. format="dd/MM/Y HH:mm")
  359. reset.likeCount = Like.objects.filter(reset=reset).count()
  360. # Timeline graph
  361. # Data pre-processing
  362. if not counter.lastReset.noSeum:
  363. resets_graph = resets
  364. for reset in resets_graph:
  365. reset.timestamp = {
  366. 'v': reset.timestamp.timestamp(),
  367. 'f': "Il y a " + format_timedelta(
  368. datetime.now() - reset.timestamp.replace(tzinfo=None),
  369. locale='fr', threshold=1)
  370. }
  371. if reset.selfSeum:
  372. reset.Seum = {'v': 0, 'f': reset.reason}
  373. else:
  374. reset.Seum = {'v': 0, 'f': 'De ' +
  375. reset.who.trigramme + ' : ' + reset.reason}
  376. # Drawing the graph
  377. data = ModelDataSource(
  378. resets, fields=['timestamp', 'Seum'])
  379. chart = gchart.LineChart(data, options={
  380. 'lineWidth': 0,
  381. 'pointSize': 10,
  382. 'title': '',
  383. 'vAxis': {'ticks': []},
  384. 'hAxis': {'ticks': [{
  385. 'v': firstReset.timestamp.timestamp(),
  386. 'f': 'Il y a ' + format_timedelta(
  387. datetime.now() - firstReset.timestamp.replace(tzinfo=None),
  388. locale='fr', threshold=1)
  389. }, {
  390. 'v': datetime.now().timestamp(),
  391. 'f': 'Présent'}
  392. ]},
  393. 'legend': 'none',
  394. 'height': 90
  395. })
  396. else:
  397. chart = None
  398. return render(request, 'counterTemplate.html', {
  399. 'counter': counter,
  400. 'chart': chart,
  401. 'resets': resets,
  402. 'seumFrequency': seumFrequency,
  403. 'myCounter': myCounter,
  404. })
  405. @login_required
  406. def hashtag(request, keyword):
  407. try:
  408. keyword = Keyword.objects.get(text=keyword)
  409. except Keyword.DoesNotExist:
  410. print('erreur !')
  411. return HttpResponseRedirect(reverse('home'))
  412. hashtag = '#'+keyword.text
  413. resets = Reset.objects.filter(hashtag__keyword=keyword)
  414. for reset in resets:
  415. if (reset.who is None or
  416. reset.who.id == reset.counter.id):
  417. reset.selfSeum = True
  418. else:
  419. reset.selfSeum = False
  420. reset.date = format_datetime(
  421. reset.timestamp, locale='fr',
  422. format="dd/MM/Y HH:mm")
  423. reset.likeCount = Like.objects.filter(reset=reset).count()
  424. return render(request, 'hashtagTemplate.html', {
  425. 'hashtag': hashtag,
  426. 'totalNumber': resets.count(),
  427. 'resets': resets,
  428. })
  429. def createUser(request):
  430. if (request.method == 'POST'):
  431. # create a form instance and populate it with data from the request:
  432. data = dict(request.POST)
  433. email = data['email'][0]
  434. username = email.split('@')[0]
  435. trigramme = data['trigramme'][0]
  436. nick = data['nick'][0]
  437. password1 = data['password1'][0]
  438. password2 = data['password2'][0]
  439. email_notifications = ('email_notifications' in data.keys())
  440. if password1 != password2:
  441. error = "Les deux mots de passe sont différents."
  442. return render(request, 'createUser.html', {'error': error})
  443. try:
  444. test_user = User.objects.get(email=email)
  445. error = "Un utilisateur avec cette adresse email existe déjà !"
  446. return render(request, 'createUser.html', {'error': error})
  447. except User.DoesNotExist:
  448. try:
  449. user = User.objects.create_user(username, email, password1)
  450. except IntegrityError:
  451. error = "Utilise une autre adresse email, un autre utilisateur \
  452. a le même login que toi."
  453. return render(request, 'createUser.html', {'error': error})
  454. counter = Counter()
  455. counter.name = nick
  456. counter.email = email
  457. counter.trigramme = trigramme
  458. counter.user = user
  459. counter.email_notifications = False
  460. counter.save()
  461. return render(request, 'createUserDone.html', {'login': username})
  462. else:
  463. return render(request, 'createUser.html', {'error': None})
  464. @login_required
  465. def toggleEmailNotifications(request):
  466. counter = Counter.objects.get(user=request.user)
  467. counter.email_notifications = not counter.email_notifications
  468. counter.save()
  469. return HttpResponseRedirect(reverse('home'))
  470. @login_required
  471. def toggleScoreSorting(request):
  472. counter = Counter.objects.get(user=request.user)
  473. counter.sort_by_score = not counter.sort_by_score
  474. counter.save()
  475. return HttpResponseRedirect(reverse('home'))
  476. @login_required
  477. def like(request):
  478. if (request.method == 'POST'):
  479. # create a form instance and populate it with data from the request:
  480. data = dict(request.POST)
  481. liker = Counter.objects.get(pk=data['liker'][0])
  482. reset = Reset.objects.get(pk=data['reset'][0])
  483. like = Like()
  484. like.liker = liker
  485. like.reset = reset
  486. like.save()
  487. return HttpResponseRedirect(data['redirect'][0])