views.py 21 KB

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