views.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. from datetime import datetime, timedelta
  2. from django.contrib.auth import logout, login, update_session_auth_hash
  3. from django.contrib.auth.decorators import login_required
  4. from django.contrib.auth.forms import PasswordChangeForm
  5. from django.contrib.auth.models import User
  6. from django.core.paginator import Paginator
  7. from django.db.models.aggregates import Sum
  8. from django.http.response import HttpResponse
  9. from django.shortcuts import render, get_object_or_404, redirect
  10. from django.urls.base import reverse
  11. from notifications.models import Notification
  12. from notifications.signals import notify
  13. from main.forms import StoryForm, EpicForm, RegisterForm, ProfileForm, \
  14. CommentForm, SprintForm, NewSprintForm
  15. from main.models import Story, Epic, Sprint, Comment, Project
  16. def register(request):
  17. if request.method == 'POST':
  18. form = RegisterForm(request.POST)
  19. if form.is_valid():
  20. user = form.save()
  21. login(request, user)
  22. return redirect("index")
  23. else:
  24. form = RegisterForm()
  25. return render(request, 'registration/register.html', {'form': form})
  26. @login_required
  27. def index(request):
  28. epics = Epic.objects.filter(closed=False)
  29. return render(request, 'index.html', {'current_sprint': Sprint.current(), 'epics': epics})
  30. @login_required
  31. def profile_update(request):
  32. if request.method == 'POST':
  33. user = get_object_or_404(User, username=request.user)
  34. form = ProfileForm(request.POST, instance=user)
  35. if form.is_valid():
  36. user = form.save()
  37. login(request, user)
  38. return redirect("index")
  39. else:
  40. user = get_object_or_404(User, username=request.user)
  41. form = ProfileForm(instance=user)
  42. return render(request, 'registration/register.html', {'form': form})
  43. @login_required
  44. def change_password(request):
  45. if request.method == 'POST':
  46. form = PasswordChangeForm(request.user, request.POST)
  47. if form.is_valid():
  48. user = form.save()
  49. update_session_auth_hash(request, user) # Important!
  50. return redirect('index')
  51. else:
  52. form = PasswordChangeForm(request.user)
  53. return render(request, 'registration/change_password.html', {'form': form})
  54. @login_required
  55. def logout(request):
  56. logout(request)
  57. return redirect("index")
  58. @login_required
  59. def backlog_editor(request):
  60. epics = Epic.objects.filter(closed=False)
  61. closed = Epic.objects.filter(closed=True)
  62. return render(request, 'backlog_editor.html', {'epics': epics, 'closed': closed})
  63. def sprint_new(request):
  64. if request.method == 'POST':
  65. form = NewSprintForm(request.POST)
  66. if form.is_valid():
  67. form.save()
  68. return redirect("sprint_end")
  69. else:
  70. sprint = Sprint()
  71. current_sprint = Sprint.current()
  72. sprint.number = current_sprint.number + 1
  73. new_start = current_sprint.date_end + timedelta(days=1)
  74. while new_start.weekday() >= 5:
  75. new_start = new_start + timedelta(days=1)
  76. new_end = new_start + timedelta(days=13)
  77. while new_end.weekday() >= 5:
  78. new_end = new_end - timedelta(days=1)
  79. sprint.date_start = new_start.strftime('%d/%m/%Y')
  80. sprint.date_end = new_end.strftime('%d/%m/%Y')
  81. form = NewSprintForm(instance=sprint)
  82. return render(request, 'sprint_new.html', {'form': form })
  83. def sprint_end(request):
  84. current_sprint = Sprint.current()
  85. next_sprint = Sprint.next()
  86. if not next_sprint:
  87. return redirect("sprint_new")
  88. if request.method == 'POST':
  89. current_sprint.retro = request.POST["retro"]
  90. current_sprint.closed = True
  91. current_sprint.save()
  92. notify_sprint_end(current_sprint, next_sprint, request.user)
  93. return redirect("index")
  94. form = SprintForm(instance=current_sprint)
  95. return render(request, 'sprint_end.html', {'sprint': current_sprint, 'next_sprint': next_sprint, 'form': form})
  96. @login_required
  97. def story_index(request):
  98. sprints = Sprint.objects.all()
  99. users = User.objects.all()
  100. stories = Story.objects
  101. filters = request.GET
  102. if 'state' in filters and filters['state']:
  103. stories = stories.filter(closed=(filters['state'] == 'closed'))
  104. if 'sprint' in filters and filters['sprint']:
  105. if filters['sprint'] == "None":
  106. stories = stories.filter(sprints=None)
  107. else:
  108. stories = stories.filter(sprints__id=filters['sprint'])
  109. if 'author' in filters and filters['author']:
  110. stories = stories.filter(author_id=filters['author'])
  111. if 'assignee' in filters and filters['assignee']:
  112. stories = stories.filter(assignees__id=filters['assignee'])
  113. count = stories.count()
  114. total_weight = stories.aggregate(Sum('weight'))['weight__sum']
  115. paginator = Paginator(stories.all(), 20)
  116. page = request.GET.get('page')
  117. stories = paginator.get_page(page)
  118. return render(request, 'story_index.html', {'stories': stories,
  119. 'count': count,
  120. 'total_weight': total_weight,
  121. 'sprints': sprints,
  122. 'users': users,
  123. 'pages': range(1, paginator.num_pages + 1)})
  124. @login_required
  125. def story_details(request, story_id):
  126. story = get_object_or_404(Story, id=story_id)
  127. from_ = request.GET['from'] if ('from' in request.GET and request.GET['from']) else 'epic_details';
  128. return render(request, 'story_details.html', {'story': story, 'from': from_})
  129. @login_required
  130. def story_create(request, epic_id=None):
  131. if request.method == 'POST':
  132. form = StoryForm(request.POST)
  133. if form.is_valid():
  134. story = form.save()
  135. for assignee in story.assignees.all():
  136. if assignee != request.user:
  137. notify_story_assigned(story, request.user, assignee)
  138. return redirect("story_details", story.id)
  139. else:
  140. story = Story()
  141. if epic_id is not None:
  142. story.epic = get_object_or_404(Epic, id=epic_id)
  143. story.author = User.objects.get(username=request.user)
  144. form = StoryForm(instance=story)
  145. return render(request, 'story_form.html', {'form': form, 'current_sprint_id': Sprint.current().id})
  146. @login_required
  147. def story_edit(request, story_id):
  148. if request.method == 'POST':
  149. story = get_object_or_404(Story, id=story_id)
  150. former_assignees = list(story.assignees.all())
  151. form = StoryForm(request.POST, instance=story)
  152. if form.is_valid():
  153. form.save()
  154. for assignee in form.instance.assignees.all():
  155. if not assignee in former_assignees and assignee.id != request.user.id:
  156. notify_story_assigned(story, request.user, assignee)
  157. return redirect("story_details", story.id)
  158. else:
  159. story = get_object_or_404(Story, id=story_id)
  160. form = StoryForm(instance=story)
  161. return render(request, 'story_form.html', {'form': form, 'current_sprint_id': Sprint.current().id})
  162. @login_required
  163. def story_delete(request, story_id):
  164. if request.method == 'POST':
  165. story = get_object_or_404(Story, id=story_id)
  166. story.delete()
  167. return redirect("index")
  168. else:
  169. story = get_object_or_404(Story, id=story_id)
  170. return render(request, 'deletion.html', {'object': story})
  171. @login_required
  172. def story_close(request, story_id):
  173. story = get_object_or_404(Story, id=story_id)
  174. story.close()
  175. notify_story_closed(story, request.user)
  176. if request.is_ajax():
  177. return HttpResponse(story.to_json())
  178. else:
  179. return redirect(request.META['HTTP_REFERER'])
  180. @login_required
  181. def story_reaffect(request, story_id):
  182. story = get_object_or_404(Story, id=story_id)
  183. next_sprint = Sprint.next()
  184. story.sprints.add(next_sprint)
  185. story.save()
  186. if request.is_ajax():
  187. return HttpResponse(story.to_json())
  188. else:
  189. return redirect('story_details', story.id)
  190. @login_required
  191. def story_reopen(request, story_id):
  192. story = get_object_or_404(Story, id=story_id)
  193. story.reopen()
  194. notify_story_reopened(story, request.user)
  195. return redirect(request.META['HTTP_REFERER'])
  196. @login_required
  197. def epic_details(request, epic_id):
  198. epic = get_object_or_404(Epic, id=epic_id)
  199. return render(request, 'epic_details.html', {'epic': epic})
  200. @login_required
  201. def epic_create(request):
  202. if request.method == 'POST':
  203. form = EpicForm(request.POST)
  204. if form.is_valid():
  205. epic = form.save(commit=False)
  206. epic.author = User.objects.get(username=request.user)
  207. epic.save()
  208. return redirect("backlog_editor")
  209. else:
  210. form = EpicForm()
  211. return render(request, 'epic_form.html', {'form': form})
  212. @login_required
  213. def epic_edit(request, epic_id, from_=""):
  214. if request.method == 'POST':
  215. epic = get_object_or_404(Epic, id=epic_id)
  216. form = EpicForm(request.POST, instance=epic)
  217. if form.is_valid():
  218. form.save()
  219. if from_:
  220. return redirect(from_)
  221. else:
  222. return redirect("epic_details", epic.id)
  223. else:
  224. epic = get_object_or_404(Epic, id=epic_id)
  225. form = EpicForm(instance=epic)
  226. return render(request, 'epic_form.html', {'form': form})
  227. @login_required
  228. def epic_delete(request, epic_id):
  229. if request.method == 'POST':
  230. epic = get_object_or_404(Epic, id=epic_id)
  231. epic.delete()
  232. return redirect("index")
  233. else:
  234. epic = get_object_or_404(Epic, id=epic_id)
  235. return render(request, 'deletion.html', {'object': epic})
  236. @login_required
  237. def epic_value_update(request, epic_id):
  238. if request.method == 'POST':
  239. epic = get_object_or_404(Epic, id=epic_id)
  240. epic.value = request.POST["value"]
  241. epic.save()
  242. return redirect("backlog_editor")
  243. @login_required
  244. def epic_close(request, epic_id):
  245. epic = get_object_or_404(Epic, id=epic_id)
  246. epic.close()
  247. epic.save()
  248. notify_epic_closed(epic, request.user)
  249. return redirect("backlog_editor")
  250. @login_required
  251. def epic_reopen(request, epic_id):
  252. epic = get_object_or_404(Epic, id=epic_id)
  253. epic.reopen()
  254. notify_epic_reopened(epic, request.user)
  255. return redirect("backlog_editor")
  256. @login_required
  257. def reports(request):
  258. return render(request, 'reports/report_index.html')
  259. @login_required
  260. def report_sprints(request):
  261. sprints = Sprint.objects.all()
  262. return render(request, 'reports/report_sprints.html', {'sprints': sprints})
  263. @login_required
  264. def report_projects(request):
  265. epics = Epic.objects.filter(closed=False)
  266. return render(request, 'reports/report_projects.html', {'epics': epics})
  267. def report_activity(request):
  268. projects_activity = {}
  269. for project in Project.objects.all():
  270. projects_activity[project] = {}
  271. projects_activity[project]["current"] = 0
  272. projects_activity[project]["sixmonths"] = 0
  273. epics_activity = {}
  274. for epic in Epic.objects.all():
  275. epics_activity[epic] = {}
  276. epics_activity[epic]["current"] = 0
  277. epics_activity[epic]["sixmonths"] = 0
  278. current = True
  279. for sprint in Sprint.objects.filter(date_end__lt = datetime.datetime.today()).order_by('-date_start')[:12]:
  280. for story in sprint.stories.all():
  281. if not story.epic:
  282. continue
  283. if current:
  284. projects_activity[story.epic.project]["current"] += story.weight
  285. epics_activity[story.epic]["current"] += story.weight
  286. projects_activity[story.epic.project]["sixmonths"] += story.weight
  287. epics_activity[story.epic]["sixmonths"] += story.weight
  288. current = False
  289. epics_activity_cleaned = {epic: act for epic, act in epics_activity.items() if act["sixmonths"] > 0 }
  290. return render(request, 'reports/report_activity.html', {'projects_activity': projects_activity, 'epics_activity': epics_activity_cleaned})
  291. # comments
  292. @login_required
  293. def comment_post(request):
  294. form = CommentForm(request.POST)
  295. if form.is_valid():
  296. comment = form.save(commit=False)
  297. comment.author = request.user
  298. comment.save()
  299. notify_comment(comment)
  300. return redirect(request.META['HTTP_REFERER'].split("#")[0] + "#a-comment-{}".format(comment.id))
  301. else:
  302. return redirect(request.META['HTTP_REFERER'])
  303. @login_required
  304. def comment_edit(request, comment_id):
  305. comment = get_object_or_404(Comment, id=comment_id)
  306. form = CommentForm(request.POST, instance=comment)
  307. if form.is_valid():
  308. form.save()
  309. return redirect(request.META['HTTP_REFERER'].split("#")[0] + "#a-comment-{}".format(comment_id))
  310. else:
  311. return redirect(request.META['HTTP_REFERER'])
  312. @login_required
  313. def comment_del(request, comment_id):
  314. comment = get_object_or_404(Comment, id=comment_id)
  315. comment.delete()
  316. return redirect(request.META['HTTP_REFERER'].split("#")[0] + "#a-comment-section")
  317. @login_required
  318. def search(request):
  319. qstr = request.GET["q"]
  320. results = []
  321. results += Epic.objects.filter(name__icontains=qstr)
  322. results += Story.objects.filter(name__icontains=qstr)
  323. results += Epic.objects.filter(description__icontains=qstr)
  324. results += Story.objects.filter(description__icontains=qstr)
  325. if len(results) == 1:
  326. r = results[0]
  327. if isinstance(r, Epic):
  328. return redirect("epic_details", r.id)
  329. else:
  330. return redirect("story_details", r.id)
  331. else:
  332. paginator = Paginator(results, 10)
  333. page = request.GET.get('page')
  334. results = paginator.get_page(page)
  335. return render(request, 'search_results.html', {'results': results, 'pages': range(1, paginator.num_pages + 1)})
  336. # notifications
  337. @login_required
  338. def notif_seen(_, notif_id):
  339. notif = get_object_or_404(Notification, id=notif_id)
  340. notif.mark_as_read()
  341. return HttpResponse('{}')
  342. @login_required
  343. def notif_all_seen(request):
  344. for notif in request.user.notifications.all():
  345. notif.mark_as_read()
  346. return HttpResponse('{}')
  347. def notify_comment(comment):
  348. obj = comment.content_object
  349. target_url = reverse(f"{obj.model_name().lower()}_details", args=(obj.id,))
  350. notif_content= f"{comment.author.username} a commenté <a href='{target_url}#a-comment-{comment.id}' title='{obj.name}'>{obj.model_name()} #{obj.id}</a>"
  351. for user in obj.contributors():
  352. if user.id != comment.author.id:
  353. notify.send(comment.author, recipient=user, action_object = obj, verb=notif_content)
  354. def notify_story_closed(story, closed_by):
  355. target_url = reverse(f"story_details", args=(story.id,))
  356. notif_content= f"La <a href='{target_url}' title='{story.name}'>story #{story.id}</a> a été <b>clôturée</b> par {closed_by}"
  357. notify.send(closed_by, recipient=story.contributors(), action_object = story, verb=notif_content)
  358. def notify_story_reopened(story, opened_by):
  359. target_url = reverse(f"story_details", args=(story.id,))
  360. notif_content= f"La <a href='{target_url}' title='{story.name}'>story #{story.id}</a> a été <b>réouverte</b> par {opened_by}"
  361. notify.send(opened_by, recipient=story.contributors(), action_object = story, verb=notif_content)
  362. def notify_epic_closed(epic, closed_by):
  363. target_url = reverse(f"epic_details", args=(epic.id,))
  364. notif_content= f"La <a href='{target_url}' title='{epic.name}'>story #{epic.id}</a> a été <b>clôturée</b> par {closed_by}"
  365. notify.send(closed_by, recipient=User.objects.all(), action_object = epic, verb=notif_content)
  366. def notify_epic_reopened(epic, opened_by):
  367. target_url = reverse(f"epic_details", args=(epic.id,))
  368. notif_content= f"L'<a href='{target_url}' title='{epic.name}'>epic #{epic.id}</a> a été <b>réouverte</b> par {opened_by}"
  369. notify.send(opened_by, recipient=User.objects.all(), action_object = epic, verb=notif_content)
  370. def notify_story_assigned(story, assigned_by, assigned_to):
  371. target_url = reverse(f"story_details", args=(story.id,))
  372. notif_content= f"La <a href='{target_url}' title='{story.name}'>story #{story.id}</a> vous a été assignée par {assigned_by}"
  373. notify.send(assigned_by, recipient=assigned_to, action_object = story, verb=notif_content)
  374. def notify_sprint_end(sprint, next_sprint, ended_by):
  375. target_url = reverse(f"report_sprints")
  376. notify.send(ended_by, recipient=User.objects.all(), action_object = sprint, verb=f"Fin du <a href='{target_url}'>{sprint}</a>")
  377. notify.send(ended_by, recipient=User.objects.all(), action_object = sprint, verb=f"Démarrage du <a href='{target_url}'>{next_sprint}</a>")