Aller au contenu

Un début d'implémentation de `todo.txt` avec Django

·811 mots·4 mins
Sommaire

Un petit début d’implémentation de todo.txt avec Django.

Implémentation
#

# app/models.py

from datetime import date
import re

from django.db import models
from django.contrib.auth.models import User


class Project(models.Model):
    """Décrit un projet.

    Attributes:
        name (str): Le nom du projet en cours.
    """

    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class Context(models.Model):
    """Décrit un contexte.

    Attributes:
        name (str): Le nom du contexte.
    """

    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class Task(models.Model):
    """Décrit quelque chose qui doit être fait :)

    Attributes:
        description (str): La description complète.
        projects (Project): La liste des projets qui sont liés à la tâche en cours.
        contexts (Context): La liste des contextes qui sont liés à la tâche en cours.
        priority (str): Le niveau de priorité (A, B, C, ...)
        deadline (date): L'échéance à laquelle la tâche doit avoir été effectuée.
        creator (User): L'utilisateur qui a créé la tâche.
        completed (boolean): Indique si la tâche est terminée ou pas.
        completion_date (date): La date à laquelle la tâche a été clôturée.
    """

    description = models.CharField(max_length=2000)
    projects = models.ManyToManyField('Project')
    contexts = models.ManyToManyField('Context')
    priority = models.CharField(max_length=1, null=True)
    deadline = models.DateField(null=True)
    creator = models.ForeignKey(User, null=True)
    completed = models.BooleanField(default=False)
    completion_date = models.DateTimeField(null=True)

    @property
    def iscomplete(self):
        """Vérifie que la tâche est terminée.

        La méthode se base sur la présence d'un `x` au début de la ligne.
        """
        return self.description.startswith('x ')

    @staticmethod
    def create(description):
        """Crée une nouvelle tâche, en se basant sur sa description.

        Pour rappel, les spécifications de `todo.txt` sont dispos ici:
            * http://todotxt.com/
        """

        t = Task()
        t.description = description
        t.priority = t.__buildpriority()
        t.date = t.__builddate()
        t.completed = t.iscomplete
        t.save()

        for project_name in t.buildprojects():
            p, insert_result = Project.objects.get_or_create(name=project_name)
            t.projects.add(p)

        for context_name in t.buildcontexts():
            c, insert_result = Context.objects.get_or_create(name=context_name)
            t.contexts.add(c)

        return t

    def complete(self, adddate=False):
        """Clôture une tâche, en y ajoutant soit la date de clôture, soit un `x`.
        """
        if not self.iscomplete:
            if adddate:
                today = date.today().strftime('%Y-%m-%d') + ' '
                self.description = 'x ' + today + self.description
            else:
                self.description = 'x ' + self.description
            self.completed = True
            self.save()

    def __buildpriority(self):
        """Retourne la priorité de la tâche, en se basant sur sa description.

        La méthode regarde si
        * la tâche débute par une lettre majuscule [A-Z],
        * est suivie d'un espace.
        """
        re_result = re.findall(r'^\([A-Z]\) ', self.description)
        if re_result:
            return re_result[0].strip()

        return None

    def __builddate(self):
        """Récupère l'échéance depuis la description.

        L'échéance se trouve *après* la définition de la priorité.
        On recherche une chaîne représentée par une date au format YYYY-MM-DD.
        """
        re_result = re.findall(r'^(\([A-Z]\) )?[0-9]{4}-[0-9]{2}-[0-9]{2}',
                    self.description)
        if re_result:
            return date(2011, 9, 9)

        return None

    def __str__(self):
        """Retourne la description de la tâche."""
        return self.description

    def __buildvars(self, char):
        """Retourne tous les mots précédés du paramètre `char` dans la description.

        Example:
            self.description = "blabla @truc @machin #chose"
            print(self.__buildvars('@'))
            >>>> ['truc', 'machin']
            print(self.__buildvars('#'))
            >>>> ['chose']
        """
        return [x[1:] for x in re.findall(r'[%s]\w+' % (char,), self.description)]

    def buildprojects(self):
        """Récupère tous les projets associés à la tâche en cours."""
        return self.__buildvars('+')

    def buildcontexts(self):
        """Récupère tous les contextes associés à la tâche en cours."""
        return self.__buildvars('@')

Tests unitaires
#

# app/tests.py

from datetime import date

from django.test import TestCase

from potatoe.models import Task


class TaskTestCase(TestCase):

    def setUp(self):
        pass

    def test_build_task_projects(self):
        """Récupère les projets liés à une tâche."""
        t = Task.create("(A) Todo rps blablab +RPS +SharePoint")
        projects = [str(p) for p in t.projects.all()]
        self.assertIn('RPS', projects)
        self.assertIn('SharePoint', projects)

    def test_build_task_contexts(self):
        """Récupère tous les contextes liés à une tâche."""
        t = Task.create("(B) Todo bidule @brol @machin +RPS")
        contexts = [str(c) for c in t.contexts.all()]
        self.assertIn('brol', contexts)
        self.assertIn('machin', contexts)
        self.assertNotIn('RPS', contexts)

    def test_priorities(self):
        """
        Rule 1: If priority exists, it ALWAYS appears first.

        The priority is an uppercase character from A-Z enclosed
        in parentheses and followed by a space.
        """
        t = Task.create("Really gotta call Mom (A) @phone @someday")
        self.assertIsNone(t.priority)

        t = Task.create("(b) Get back to the boss")
        self.assertIsNone(t.priority)

        t = Task.create("(B)->Submit TPS report")
        self.assertIsNone(t.priority)

    def test_dates(self):
        """
        Rule 2: A task’s creation date may optionally appear
        directly after priority and a space.

        If there is no priority, the creation date appears first.
        If the creation date exists, it should be in the format YYYY-MM-DD.
        """

        t = Task.create("2011-03-02 Document +TodoTxt task format")
        self.assertEqual(t.deadline, date(2011, 3, 2))

        t.description = "(A) 2011-03-02 Call Mom"
        self.assertEqual(t.deadline, date(2011, 3, 2))

        t.description = "(A) Call Mom 2011-03-02"
        self.assertIsNone(t.deadline, None)

    def test_contexts_and_projects(self):
        """
        Rule 3: Contexts and Projects may appear anywhere in the line
        after priority/prepended date.
        """
        t = Task.create("(A) Todo rps blablab +RPS +SharePoint")
        projects = [str(p) for p in t.projects.all()]

        self.assertIn('RPS', projects)
        self.assertIn('SharePoint', projects)

        t = Task.create("(A) Todo rps blablab @phone @email")
        contexts = [str(c) for c in t.contexts.all()]
        self.assertIn('phone', contexts)
        self.assertIn('email', contexts)

    def tearDown(self):
        pass


class TestCompleteTasks(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_complete_without_date(self):
        t = Task.create("Some task @machin @brol +chose")
        t.complete()
        self.assertTrue(t.iscomplete)
        self.assertTrue(t.description.startswith('x'))

        t = Task.create("xylophone lesson")
        self.assertFalse(t.iscomplete)

        t.description = "X 2012-01-01 Make resolutions"
        self.assertFalse(t.iscomplete)

        t.description = "(A) x Find ticket prices"
        self.assertFalse(t.iscomplete)

    def test_complete_with_date(self):
        t = Task.create("Some task @machin @brol #chose")
        t.complete(True)

        value = 'x ' + date.today().strftime('%Y-%m-%d')
        beginstr = t.description[0:12]

        self.assertEqual(value, beginstr)