Commit 5ca00500 authored by Élie Bouttier's avatar Élie Bouttier

gestion des tâches

parent 7e060cf2
......@@ -40,6 +40,7 @@ INSTALLED_APPS = [
'services',
'banking',
'stocking',
'todo',
'djadhere',
'bootstrap4',
......
.container {
padding-top: 2rem;
padding-bottom: 2rem;
}
......@@ -15,6 +15,9 @@
{% block js %}
{% bootstrap_javascript jquery="full" %}
{% endblock %}
{% block extrahead %}{% endblock extrahead %}
{% block extra_js %}{% endblock extra_js %}
</head>
<body>
......
......@@ -14,6 +14,8 @@
{% endblock %}
{% block body %}
{% block pageheader %}
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" href="#">tetaneutral.net</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarTop" aria-expanded="false" aria-controls="navbarTop" aria-label="Toggle navigation">
......@@ -40,6 +42,11 @@
</li>
{% endif %}
{% endfor %}
<li class="nav-item{% block todotab %}{% endblock %}">
<a class="nav-link" href="{% url 'todo:list-tasklists' %}">
<span class="glyphicon glyphicon-heart-empty"></span>&nbsp;Liste des tâches
</a>
</li>
</ul>
<ul class="navbar-nav">
{% if request.user.is_staff %}
......@@ -60,6 +67,8 @@
</ul>
</div>
</nav>
</header>
{% endblock %}
{% block container %}
<main class="container">
......
......@@ -13,16 +13,17 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.urls import include, path
from django.contrib.gis import admin
from django.conf import settings
urlpatterns = [
url(r'^accounts/', include('accounts.urls')),
url(r'^', include('services.urls')),
url(r'^', include('adhesions.urls')),
url(r'^admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('todo/', include('todo.urls')),
path('', include('services.urls')),
path('', include('adhesions.urls')),
path('admin/', admin.site.urls),
]
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
......
default_app_config = 'todo.apps.TodoConfig'
from django.contrib import admin
from .models import TaskList, Task, TaskComment
class TaskListAdmin(admin.ModelAdmin):
list_display = ('name',)
prepopulated_fields = {'slug': ('name',)}
admin.site.register(TaskList, TaskListAdmin)
from django.apps import AppConfig
class TodoConfig(AppConfig):
name = 'todo'
verbose_name = 'Tâches'
from django.shortcuts import get_object_or_404
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from functools import wraps
from .models import TaskList
def allowed_tasklist_required(view_func):
def wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated:
return login_required(view_func)(request, *args, **kwargs)
tasklist_slug = kwargs.pop('tasklist_slug')
tasklist = get_object_or_404(TaskList, slug=tasklist_slug)
if not request.user.is_superuser \
and not request.user.groups.all().intersection(tasklist.groups.all()):
raise PermissionDenied
kwargs['tasklist'] = tasklist
return view_func(request, **kwargs)
return wraps(view_func)(wrapped_view)
from django import forms
from adhesions.models import User
from .models import Task, TaskComment
class TaskForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
tasklist = kwargs.pop('tasklist')
super().__init__(*args, **kwargs)
if tasklist.groups.exists():
members = User.objects.filter(groups__in=tasklist.groups.all())
else:
members = User.objects.all()
members = members.order_by('adhesion__id')
members = members.select_related('adhesion')
self.fields['assigned_to'].queryset = members
self.fields["assigned_to"].label_from_instance = lambda obj: "ADT%d %s" % (
obj.adhesion.id,
str(obj.profile),
)
class Meta:
model = Task
fields = ('title', 'note', 'due_date', 'assigned_to',)
widgets = {
'due_date': forms.DateInput(attrs={"type": "date"}),
}
class CommentForm(forms.ModelForm):
class Meta:
model = TaskComment
fields = ('body',)
# Generated by Django 2.2 on 2019-04-19 22:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('auth', '0011_update_proxy_permissions'),
]
operations = [
migrations.CreateModel(
name='Task',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=140, verbose_name='titre')),
('created_date', models.DateField(blank=True, default=django.utils.timezone.now, null=True)),
('due_date', models.DateField(blank=True, null=True, verbose_name='due pour le')),
('completed_date', models.DateField(blank=True, null=True)),
('note', models.TextField(blank=True, null=True)),
('priority', models.PositiveIntegerField(blank=True, null=True)),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_task_set', to=settings.AUTH_USER_MODEL, verbose_name='assignée à')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='created_task_set', to=settings.AUTH_USER_MODEL, verbose_name='créée par')),
],
options={
'verbose_name': 'tâche',
'verbose_name_plural': 'tâches',
'ordering': ['priority', 'created_date'],
},
),
migrations.CreateModel(
name='TaskList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=60, verbose_name='nom')),
('slug', models.SlugField(unique=True)),
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='groupe')),
],
options={
'verbose_name': 'liste de tâches',
'verbose_name_plural': 'listes de tâches',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='TaskComment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(default=django.utils.timezone.now)),
('body', models.TextField(blank=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='todo.Task')),
],
),
migrations.AddField(
model_name='task',
name='task_list',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='todo.TaskList'),
),
]
from django.db import models
from django.utils import timezone
from django.conf import settings
from django.contrib.auth.models import Group
import datetime
import textwrap
class TaskList(models.Model):
name = models.CharField(max_length=60, verbose_name='nom')
slug = models.SlugField(null=False, blank=False, unique=True)
groups = models.ManyToManyField(Group, verbose_name='groupe', blank=True)
@property
def completed_task_set(self):
return self.task_set.filter(completed_date__isnull=False)
@property
def uncompleted_task_set(self):
return self.task_set.filter(completed_date__isnull=True)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
verbose_name = 'liste de tâches'
verbose_name_plural = 'listes de tâches'
class Task(models.Model):
title = models.CharField(max_length=140, verbose_name='titre')
task_list = models.ForeignKey(TaskList, on_delete=models.PROTECT)
created_date = models.DateField(default=timezone.now, blank=True, null=True)
due_date = models.DateField(blank=True, null=True, verbose_name='due pour le')
completed_date = models.DateField(blank=True, null=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='created_task_set',
verbose_name='créée par',
)
assigned_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='assigned_task_set',
verbose_name='assignée à',
)
note = models.TextField(blank=True, null=True)
priority = models.PositiveIntegerField(blank=True, null=True)
def overdue_status(self):
"Returns whether the Tasks's due date has passed or not."
if self.due_date and datetime.date.today() > self.due_date:
return True
def __str__(self):
return self.title
class Meta:
ordering = ['priority', 'created_date']
verbose_name = 'tâche'
verbose_name_plural = 'tâches'
class TaskComment(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='comments')
date = models.DateTimeField(default=timezone.now)
body = models.TextField(blank=True)
@property
def snippet(self):
body_snippet = textwrap.shorten(self.body, width=35, placeholder="...")
return "{author} - {snippet}...".format(author=str(self.author), snippet=body_snippet)
def __str__(self):
return self.snippet
{% extends "base.html" %}
{% block todotab %} active{% endblock %}
{% extends "todo/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="card-deck">
<div class="card col-sm-8">
<div class="card-body">
<h3 class="card-title">{{ task.title }}</h3>
{% if task.note %}
<div class="card-text">{{ task.note|safe|urlize|linebreaks }}</div>
{% endif %}
</div>
</div>
<div class="card col-sm-4 p-0">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<a href="{% url 'todo:edit-task' tasklist.slug task.id %}" class="btn btn-sm btn-primary">Éditer</a>
<form method="post" action="{% url "todo:toggle-task-done" tasklist.slug task.id %}" role="form" class="d-inline">
{% csrf_token %}
<div style="display:inline;">
<button class="btn btn-info btn-sm" type="submit" name="toggle_done">
{% if task.completed_date %} Marquer non terminée {% else %} Marquer terminée {% endif %}
</button>
</div>
</form>
{% comment %}
<form method="post" action="{% url "todo:delete-task" tasklist.slug task.id %}" role="form" class="d-inline">
{% csrf_token %}
<div style="display:inline;">
<button class="btn btn-danger btn-sm" type="submit" name="submit_delete">
Supprimer
</button>
</div>
</form>
{% endcomment %}
</li>
<li class="list-group-item">
<strong>Assignée à :</strong>
{% if task.assigned_to %}{{ task.assigned_to.profile }}{% else %}–{% endif %}
</li>
<li class="list-group-item">
<strong>Créée par :</strong> {{ task.created_by.profile }}
</li>
<li class="list-group-item">
<strong>Due pour le :</strong>
{% if task.overdue_status %}
<span class="text-danger">{{ task.due_date|default:"–" }}</span>
{% else %}
{{ task.due_date|default:"–" }}
{% endif %}
</li>
<li class="list-group-item">
<strong>Terminée le :</strong> {{ task.completed_date|default:"–" }}
</li>
<li class="list-group-item">
<strong>Dans la liste :</strong>
{% if task.completed_date %}
<a href="{% url 'todo:show-tasklist-completed' tasklist.slug %}">{{ task.task_list }}</a>
{% else %}
<a href="{% url 'todo:show-tasklist' tasklist.slug %}">{{ task.task_list }}</a>
{% endif %}
</li>
</ul>
</div>
</div>
<div class="mt-3">
<h5>Ajouter un commentaire</h5>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<textarea name="body" rows="3" class="form-control" title="" id="id_body"></textarea>
</div>
<input class="btn btn-sm btn-primary" type="submit" name="add_comment" value="Ajouter un commentaire">
</form>
</div>
<div class="task_comments mt-4">
{% if task.comments.exists %}
<h5>Commentaires</h5>
{% for comment in task.comments.all %}
<div class="mb-3 card">
<div class="card-header">
<div class="float-left">
{{ comment.author }}
</div>
<span class="float-right d-inline-block text-muted">
{{ comment.date|date:"F d Y P" }}
</span>
</div>
<div class="card-body">
{{ comment.body|safe|urlize|linebreaks }}
</div>
</div>
{% endfor %}
{% else %}
<h5>Pas de commentaire</h5>
{% endif %}
</div>
{% endblock %}
{% extends "base.html" %}
{% load bootstrap4 %}
{% block todotab %} active{% endblock %}
{% block extrahead %}
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#id_assigned_to').select2();
});
</script>
{% endblock %}
{% block content %}
<h1>{% if task %}Ajouter{% else %}Modifier{% endif %} une tâche</h1>
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
{% if task %}
{% bootstrap_button "Modifier" button_type="submit" button_class="btn-primary" %}
{% else %}
{% bootstrap_button "Ajouter" button_type="submit" button_class="btn-primary" %}
{% endif %}
{% bootstrap_button "Annuler" button_type="link" href=cancel_url %}
{% endbuttons %}
</form>
{% endblock %}
{% extends "todo/base.html" %}
{% block content %}
<a href="{% url 'todo:add-task' tasklist.slug %}" class="btn btn-primary">Ajouter une tâche</a>
<hr />
{% if task_list %}
{% if completed %}
<h1>Tâches terminées de la liste « {{ tasklist.name }} »</h1>
{% else %}
<h1>Tâches de la liste « {{ tasklist.name }} »</h1>
<p><small><i>Faites glisser les tâches pour définir les priorités.</i></small></p>
{% endif %}
<table class="table" id="tasktable">
<tr class="nodrop">
<th>Tâche</th>
<th>Créé le</th>
{% if completed %}
<th>Terminée le</th>
{% else %}
<th>Due pour le</th>
{% endif %}
<th>Créé par</th>
<th>Assigné à</th>
{% comment %}<th>Mark</th{% endcomment %}
</tr>
{% for task in task_list %}
<tr id="{{ task.id }}">
<td>
<a href="{% url 'todo:show-task' tasklist.slug task.id %}">{{ task.title|truncatewords:10 }}</a>
</td>
<td>
{{ task.created_date|date:"m/d/Y" }}
</td>
<td>
<span{% if task.overdue_status %} class="text-danger"{% endif %}>
{{ task.due_date|date:"m/d/Y"|default:"–" }}
</span>
</td>
<td>
{{ task.created_by }}
</td>
<td>
{% if task.assigned_to %}{{ task.assigned_to }}{% else %}–{% endif %}
</td>
{% comment %}
<td>
<form method="post" action="{% url "todo:task_toggle_done" task.id %}" role="form">
{% csrf_token %}
<button class="btn btn-info btn-sm" type="submit" name="toggle_done">
{% if view_completed %}
Not Done
{% else %}
Done
{% endif %}
</button>
</form>
</td>
{% endcomment %}
</tr>
{% endfor %}
</table>
{% else %}
<h4>Aucune tâche {% if completed %}terminée{% else %}en cours{% endif %} dans la liste « {{ tasklist.name }} ».</h4>
{% endif %}
{% if completed %}
<a href="{% url 'todo:show-tasklist' tasklist.slug %}" class="btn btn-sm btn-warning">Voir les tâches non terminées</a>
{% else %}
<a href="{% url 'todo:show-tasklist-completed' tasklist.slug %}" class="btn btn-sm btn-warning">Voir les tâches terminées</a>
{% endif %}
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/TableDnD/0.9.1/jquery.tablednd.js" integrity="sha256-d3rtug+Hg1GZPB7Y/yTcRixO/wlI78+2m08tosoRn7A=" crossorigin="anonymous"></script>
<script type="text/javascript">
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
function order_tasks(data) {
// The JQuery plugin tableDnD provides a serialize() function which provides the re-ordered
// data in a list. We pass that list as an object ("data") to a Django view
// to save new priorities on each task in the list.
$.ajax({
url: "{% url 'todo:reorder-tasklist' tasklist.slug %}",
type: "post",
headers: {
"X-CSRFToken": csrftoken
},
data: data,
dataType: "json"
});
return false;
};
$(document).ready(function() {
// Initialise the task table for drag/drop re-ordering
$('#tasktable').tableDnD({
onDrop: function(table, row) {
order_tasks($.tableDnD.serialize());
}
});
});
</script>
{% endblock extra_js %}
{% extends "todo/base.html" %}
{% block content %}
<h1>Liste des tâches</h1>
<p>{{ task_count }} tâche{{ task_count|pluralize }} dans {{ list_count }} liste{{ list_count|pluralize }}</p>
<ul class="list-group mb-4">
{% for list in lists %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{% url 'todo:show-tasklist' list.slug %}">{{ list.name }}</a>
<span class="badge badge-primary badge-pill">{{ list.uncompleted_task_set.count }}</span>
</li>
{% endfor %}
</ul>
{% endblock %}
from django.test import TestCase
# Create your tests here.
from django.urls import path
from . import views
app_name = 'todo'
urlpatterns = [
path('', views.tasklist_list, name='list-tasklists'),
path('<str:tasklist_slug>/', views.tasklist_detail, name='show-tasklist'),
path('<str:tasklist_slug>/completed/', views.tasklist_detail, {'completed': True}, name='show-tasklist-completed'),
path('<str:tasklist_slug>/reorder/', views.tasklist_reorder, name='reorder-tasklist'),
path('<str:tasklist_slug>/add/', views.task_form, name='add-task'),
path('<str:tasklist_slug>/<int:task_id>/', views.task_detail, name='show-task'),
path('<str:tasklist_slug>/<int:task_id>/toggle-done/', views.task_toggle_done, name='toggle-task-done'),
path('<str:tasklist_slug>/<int:task_id>/edit/', views.task_form, name='edit-task'),
path('<str:tasklist_slug>/<int:task_id>/delete/', views.task_delete, name='delete-task'),
]
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect, get_object_or_404
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.urls import reverse
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.utils import timezone
from .models import TaskList, Task
from .forms import TaskForm, CommentForm
from .decorators import allowed_tasklist_required
@login_required
def tasklist_list(request):
lists = TaskList.objects.all()
if not request.user.is_superuser:
lists = lists.filter(group__in=request.user.groups.all())
lists = lists.order_by('name')
return render(request, 'todo/tasklist_list.html', {
'lists': lists,
'task_count': Task.objects.filter(completed_date__isnull=True, task_list__in=lists).count(),
'list_count': lists.count(),
})
@allowed_tasklist_required
def tasklist_detail(request, tasklist, completed=False):
task_list = tasklist.task_set.filter(completed_date__isnull=not completed)
return render(request, 'todo/tasklist_detail.html', {
'tasklist': tasklist,
'task_list': task_list,
'completed': completed,
})
@allowed_tasklist_required
def tasklist_reorder(request, tasklist):
newtasklist = request.POST.getlist("tasktable[]")
if newtasklist:
# Re-prioritize each task in list
i = 1
for pk in newtasklist:
try:
task = Task.objects.get(task_list=tasklist, pk=pk)
task.priority = i
task.save()
i += 1
except Task.DoesNotExist:
# Can occur if task is deleted behind the scenes during re-ordering.
# Not easy to remove it from the UI without page refresh, but prevent crash.
pass
# All views must return an httpresponse of some kind ... without this we get
# error 500s in the log even though things look peachy in the browser.
return HttpResponse(status=201)
@allowed_tasklist_required
def task_form(request, tasklist, task_id=None):
if task_id:
task = get_object_or_404(Task, task_list=tasklist, pk=task_id)
redirect_url = reverse('todo:show-task', kwargs={'tasklist_slug': tasklist.slug, 'task_id': task.pk})
else:
task = None
redirect_url = reverse('todo:show-tasklist', kwargs={'tasklist_slug': tasklist.slug})
form = TaskForm(request.POST or None, tasklist=tasklist, instance=task)
if request.method == 'POST' and form.is_valid():
if task:
form.save()
messages.success(request, 'Tâche mise à jour avec succès.')
else:
task = form.save(commit=False)
task.task_list = tasklist
task.created_by = request.user
task.save()
messages.success(request, 'Tâche créée avec succès.')
return redirect(redirect_url)
return render(request, 'todo/task_form.html', {
'tasklist': tasklist,
'task': task,
'form': form,
'cancel_url': redirect_url,
})
@allowed_tasklist_required
def task_detail(request, tasklist, task_id):
task = get_object_or_404(Task, task_list=tasklist, pk=task_id)
form = CommentForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
comment = form.save(commit=False)
comment.task = task
comment.author = request.user
comment.save()
messages.success(request, 'Commentaire ajouté avec succès.')
return redirect(reverse('todo:show-task', kwargs={'tasklist_slug': tasklist.slug, 'task_id': task_id}))
return render(request, 'todo/task_detail.html', {
'tasklist': tasklist,
'task': task,
'form': form,
})