Parcourir la source

gestion des tâches

Élie Bouttier il y a 6 ans
Parent
commit
bd9b81c2f0

+ 1 - 0
djadhere/settings.py

@@ -40,6 +40,7 @@ INSTALLED_APPS = [
     'services',
     'banking',
     'stocking',
+    'todo',
     'djadhere',
 
     'bootstrap4',

+ 4 - 0
djadhere/static/css/djadhere.css

@@ -0,0 +1,4 @@
+.container {
+  padding-top: 2rem;
+  padding-bottom: 2rem;
+}

+ 3 - 0
djadhere/templates/_base.html

@@ -15,6 +15,9 @@
     {% block js %}
     {% bootstrap_javascript jquery="full" %}
     {% endblock %}
+
+    {% block extrahead %}{% endblock extrahead %}
+    {% block extra_js %}{% endblock extra_js %}
   </head>
 
   <body>

+ 9 - 0
djadhere/templates/base.html

@@ -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">

+ 6 - 5
djadhere/urls.py

@@ -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:

+ 1 - 0
todo/__init__.py

@@ -0,0 +1 @@
+default_app_config = 'todo.apps.TodoConfig'

+ 17 - 0
todo/admin.py

@@ -0,0 +1,17 @@
+from django.contrib import admin
+
+from .models import TaskList, Task, TaskComment
+
+
+class TaskListAdmin(admin.ModelAdmin):
+    list_display = ('name',)
+    prepopulated_fields = {'slug': ('name',)}
+
+
+#class TaskAdmin(admin.ModelAdmin):
+#    list_display = ('task_list', 'title',)
+#'author', 'data', 'snippet')
+
+
+admin.site.register(TaskList, TaskListAdmin)
+#admin.site.register(Task, TaskAdmin)

+ 6 - 0
todo/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class TodoConfig(AppConfig):
+    name = 'todo'
+    verbose_name = 'Tâches'

+ 21 - 0
todo/decorators.py

@@ -0,0 +1,21 @@
+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)

+ 9 - 0
todo/forms.py

@@ -0,0 +1,9 @@
+from django import forms
+
+from .models import Task
+
+
+class TaskForm(forms.ModelForm):
+    class Meta:
+        model = Task
+        fields = ('title', 'note', 'due_date',) # TODO: add assigned_to, notify

+ 65 - 0
todo/models.py

@@ -0,0 +1,65 @@
+from django.db import models
+from django.utils import timezone
+from django.conf import settings
+from django.contrib.auth.models import Group
+
+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)
+
+    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)
+    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)
+    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',
+    )
+    assigned_to = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        blank=True,
+        null=True,
+        on_delete=models.SET_NULL,
+        related_name='assigned_task_set',
+    )
+    note = models.TextField(blank=True, null=True)
+    priority = models.PositiveIntegerField(blank=True, null=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)
+    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

+ 8 - 0
todo/templates/todo/_task_edit.html

@@ -0,0 +1,8 @@
+{# Form used by both Add Task and Edit Task views #}
+{% load bootstrap4 %}
+
+<form action="" name="add_task" method="post">
+  {% csrf_token %}
+  {% bootstrap_form form %}
+  {% buttons submit="Ajouter" reset="Annuler" %}{% endbuttons %}
+</form>

+ 3 - 0
todo/templates/todo/base.html

@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block todotab %} active{% endblock %}

+ 10 - 0
todo/templates/todo/task_add.html

@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block todotab %} active{% endblock %}
+
+{% block content %}
+  <h1>Ajouter une tâche</h1>
+
+  {% include 'todo/_task_edit.html' %}
+
+{% endblock %}

+ 152 - 0
todo/templates/todo/task_detail.html

@@ -0,0 +1,152 @@
+{% extends "todo/base.html" %}
+
+{% block extrahead %}
+<style>
+.select2 {
+    width: 100% !important;
+}
+
+.select2-container {
+    min-width: 0 !important;
+}
+</style>
+{{ form.media }}
+{{ merge_form.media }}
+{% endblock %}
+
+
+
+{% 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">
+          <button
+            class="btn btn-sm btn-primary"
+            id="EditTaskButton"
+            type="button"
+            data-toggle="collapse"
+            data-target="#TaskEdit">
+            Edit Task
+          </button>
+
+          <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 %} Mark Not Done {% else %} Mark Done {% endif %}
+              </button>
+            </div>
+          </form>
+
+          <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">
+                Delete
+              </button>
+            </div>
+          </form>
+        </li>
+        <li class="list-group-item">
+          <strong>Assigned to:</strong>
+          {% if task.assigned_to %} {{ task.assigned_to.get_full_name }} {% else %} Anyone {% endif %}
+        </li>
+        <li class="list-group-item">
+          <strong>Reported by:</strong> {{ task.created_by.get_full_name }}
+        </li>
+        <li class="list-group-item">
+          <strong>Due date:</strong> {{ task.due_date }}
+        </li>
+
+        {% if task.completed %}
+          <li class="list-group-item">
+            <strong>Completed on:</strong> {{ task.completed_date}}
+          </li>
+        {% else %}
+          <li class="list-group-item">
+            <strong>Completed:</strong> {{ task.completed|yesno:"Yes,No" }}
+          </li>
+        {% endif %}
+
+        <li class="list-group-item">
+          <strong>In list:</strong>
+          <a href="{% url 'todo:show-tasklist' tasklist.slug %}">
+            {{ task.task_list }}
+          </a>
+        </li>
+      </ul>
+    </div>
+  </div>
+
+  <div id="TaskEdit" class="collapse">
+    {# Task edit / new task form #}
+    {% include 'todo/include/task_edit.html' %}
+    {% if merge_form is not None %}
+    <form action="" method="post">
+      <div class="card border-danger">
+      <div class="card-header">Merge task</div>
+      <div class="card-body">
+        <div class="">
+          <p>Merging is a destructive operation. This task will not exist anymore, and comments will be moved to the target task.</p>
+        {% csrf_token %}
+        {% for field in merge_form.visible_fields %}
+        <p>
+          {{ field.errors }}
+          {{ field }}
+        </p>
+        {% endfor %}
+        <input class="d-inline btn btn-sm btn-outline-danger" type="submit" name="merge_task_into" value="Merge">
+      </div>
+      </div>
+      </div>
+    </form>
+    {% endif %}
+  </div>
+
+  <div class="mt-3">
+    <h5>Add comment</h5>
+    <form action="" method="post">
+      {% csrf_token %}
+      <div class="form-group">
+        <textarea class="form-control" name="comment-body" rows="3" required></textarea>
+      </div>
+      <input class="btn btn-sm btn-primary" type="submit" name="add_comment" value="Add Comment">
+    </form>
+  </div>
+
+  <div class="task_comments mt-4">
+    {% if comment_list %}
+      <h5>Comments on this task</h5>
+      {% for comment in comment_list %}
+      <div class="mb-3 card">
+        <div class="card-header">
+          <div class="float-left">
+            {% if comment.email_message_id %}
+            <span class="badge badge-warning">email</span>
+            {% endif %}
+            {{ comment.author_text }}
+          </div>
+          <span class="float-right d-inline-block text-muted">
+            {{ comment.date|date:"F d Y P" }}
+          </span>
+        </div>
+        <div class="{{ comment_classes | join:" " }} card-body">
+          {{ comment.body|safe|urlize|linebreaks }}
+        </div>
+      </div>
+      {% endfor %}
+    {% else %}
+        <h5>No comments (yet).</h5>
+    {% endif %}
+  </div>
+{% endblock %}

+ 115 - 0
todo/templates/todo/tasklist_detail.html

@@ -0,0 +1,115 @@
+{% 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 tasklist.task_set %}
+      <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>
+
+      <table class="table" id="tasktable">
+        <tr class="nodrop">
+          <th>Tâche</th>
+          <th>Créé le</th>
+          <th>Due pour le</th>
+          <th>Créé par</th>
+          {% comment %}<th>Assigné à</th>{% endcomment %}
+          {% comment %}<th>Mark</th{% endcomment %}
+        </tr>
+
+        {% for task in tasklist.task_set.all %}
+          <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="overdue"{% endif %}>
+                {{ task.due_date|date:"m/d/Y"|default:"–" }}
+              </span>
+            </td>
+            <td>
+              {{ task.created_by }}
+            </td>
+            {% comment %}
+            <td>
+              {% if task.assigned_to %}{{ task.assigned_to }}{% else %}Anyone{% endif %}
+            </td>
+            <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>
+
+      {% comment %}{% include 'todo/include/toggle_delete.html' %}{% endcomment %}
+
+  {% else %}
+    <h4>Il n’y a aucune tâche dans cette liste.</h4>
+    {% comment %}{% include 'todo/include/toggle_delete.html' %}{% endcomment %}
+
+  {% 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 %}

+ 17 - 0
todo/templates/todo/tasklist_list.html

@@ -0,0 +1,17 @@
+{% 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.task_set.count }}</span>
+    </li>
+    {% endfor %}
+  </ul>
+
+{% endblock %}

+ 3 - 0
todo/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 18 - 0
todo/urls.py

@@ -0,0 +1,18 @@
+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>/delete/', views.tasklist_delete, name='delete-tasklist'),
+    path('<str:tasklist_slug>/reorder/', views.tasklist_reorder, name='reorder-tasklist'),
+    path('<str:tasklist_slug>/add/', views.task_add, 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>/delete/', views.task_delete, name='delete-task'),
+]

+ 93 - 0
todo/views.py

@@ -0,0 +1,93 @@
+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 .models import TaskList, Task
+from .forms import TaskForm
+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(task_list__in=lists).count(), ## TODO check it works properly
+        'list_count': lists.count(),
+    })
+
+
+@allowed_tasklist_required
+def tasklist_detail(request, tasklist):
+    return render(request, 'todo/tasklist_detail.html', {
+        'tasklist': tasklist,
+    })
+
+
+@allowed_tasklist_required
+def tasklist_delete(request, tasklist):
+    pass
+
+
+@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_add(request, tasklist):
+    form = TaskForm(request.POST or None)
+    if request.method == 'POST' and form.is_valid():
+        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(reverse('todo:show-tasklist', kwargs={'tasklist_slug': tasklist.slug}))
+    return render(request, 'todo/task_add.html', {
+        'tasklist': tasklist,
+        'form': form,
+    })
+
+
+@allowed_tasklist_required
+def task_detail(request, tasklist, task_id):
+    task = get_object_or_404(Task, pk=task_id)
+    return render(request, 'todo/task_detail.html', {
+        'tasklist': tasklist,
+        'task': task,
+    })
+    
+
+@allowed_tasklist_required
+def task_toggle_done(request, tasklist, task_id):
+    task = get_object_or_404(Task, pk=task_id)
+    return redirect() # TODO
+
+
+@allowed_tasklist_required
+def task_delete(request, tasklist):
+    pass