views.py 19 KB

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