Completed
Push — qml ( 9c2cb2...7f61bc )
by Olivier
58s
created

TaskSorter.default()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
1
from datetime import datetime, date, time, MAXYEAR
2
import re
3
4
from PyQt5 import QtCore
5
from enum import Enum
6
from qtodotxt.lib.task_htmlizer import TaskHtmlizer
7
8
9
class recursiveMode(Enum):
10
    completitionDate = 0  # Original due date mode: Task recurs from original due date
11
    originalDueDate = 1  # Completion date mode: Task recurs from completion date
12
13
14
class recursion:
15
    mode = None
16
    increment = None
17
    interval = None
18
19
    def __init__(self, arg_mode, arg_increment, arg_interval):
20
        self.mode = arg_mode
21
        self.increment = arg_increment
22
        self.interval = arg_interval
23
24
25
class TaskSorter(object):
26
    
27
    @staticmethod
28
    def projects(tasks):
29
        def tmp(task):
30
            return task.projects, task
31
        return sorted(tasks, key=tmp)
32
33
    @staticmethod
34
    def contexts(tasks):
35
        def tmp(task):
36
            return task.contexts, task
37
        return sorted(tasks, key=tmp)
38
39
    @staticmethod
40
    def due(tasks):
41
        def tmp(task):
42
            if task.due:
43
                return task.due, task
44
            else:
45
                return datetime(MAXYEAR, 1, 1), task
46
        return sorted(tasks, key=tmp)
47
48
    @staticmethod
49
    def default(tasks):
50
        return tasks
51
52
53
class Task(QtCore.QObject):
54
    """
55
    Represent a task as defined in todo.txt format
56
    Take a line in todo.txt format as argument
57
    Arguments are read-only, reparse string to modify them or
58
    use one the modification methods such as setCompleted()
59
    """
60
61
    modified = QtCore.pyqtSignal(object)
62
63
    def __init__(self, text):
64
        QtCore.QObject.__init__(self)
65
        self._settings = QtCore.QSettings()
66
        self._highest_priority = 'A'
67
        # all other class attributes are defined in _reset method
68
        # which is called in _parse
69
        self._parse(text)
70
71
    def addCreationCate(self):
72
        self._removeCreationDate()
73
        self._addCreationDate()
74
75
    def __str__(self):
76
        return self._text
77
78
    def __repr__(self):
79
        return "Task({})".format(self._text)
80
81
    def _removeCreationDate(self):
82
        match = re.match(r'^(\([A-Z]\)\s)?[0-9]{4}\-[0-9]{2}\-[0-9]{2}\s(.*)', self._text)
83
        if match:
84
            if match.group(1):
85
                self._text = match.group(1) + match.group(2)
86
            else:
87
                self._text = match.group(2)
88
89
    def addCreationDate(self):
90
        date_string = date.today().strftime('%Y-%m-%d')
91
        if re.match(r'^\([A-Z]\)', self._text):
92
            self._text = '%s %s %s' % (self._text[:3], date_string, self._text[4:])
93
        else:
94
            self._text = '%s %s' % (date_string, self._text)
95
96
    def _reset(self):
97
        self.contexts = []
98
        self.projects = []
99
        self._priority = ""
100
        self.is_complete = False
101
        self.completion_date = None
102
        self.creation_date = None
103
        self.is_future = False
104
        self.threshold_error = ""
105
        self._text = ''
106
        self.description = ''
107
        self.due = None
108
        self.due_error = ""
109
        self.threshold = None
110
        self.keywords = {}
111
        self.recursion = None
112
113
    def _parse(self, line):
114
        """
115
        parse a task formated as string in todo.txt format
116
        """
117
        self._reset()
118
        words = line.split(' ')
119
        if words[0] == "x":
120
            self.is_complete = True
121
            words = words[1:]
122
            # parse next word as a completion date
123
            # required by todotxt but often not here
124
            self.completion_date = self._parseDate(words[0])
125
            if self.completion_date:
126
                words = words[1:]
127
        elif re.search(r'^\([A-Z]\)$', words[0]):
128
            self._priority = words[0][1:-1]
129
            words = words[1:]
130
131
        dato = self._parseDate(words[0])
132
        if dato:
133
            self.creation_date = dato
134
            words = words[1:]
135
136
        self.description = " ".join(words)
137
        for word in words:
138
            self._parseWord(word)
139
        self._text = line
140
141
    @QtCore.pyqtProperty('QString', notify=modified)
142
    def text(self):
143
        return self._text
144
145
    @text.setter
146
    def text(self, txt):
147
        self._parse(txt)
148
        self.modified.emit(self)
149
150
    @QtCore.pyqtProperty('QString', notify=modified)
151
    def html(self):
152
        return self.toHtml()
153
154
    @QtCore.pyqtProperty('QString', notify=modified)
155
    def priority(self):
156
        return self._priority
157
158
    @QtCore.pyqtProperty('QString', notify=modified)
159
    def priorityHtml(self):
160
        htmlizer = TaskHtmlizer()
161
        return htmlizer._htmlizePriority(self.priority)
162
163
    def _parseWord(self, word):
164
        if len(word) > 1:
165
            if word.startswith('@'):
166
                self.contexts.append(word[1:])
167
            elif word.startswith('+'):
168
                self.projects.append(word[1:])
169
            elif ":" in word:
170
                self._parseKeyword(word)
171
172
    def _parseKeyword(self, word):
173
        key, val = word.split(":", 1)
174
        self.keywords[key] = val
175
        if word.startswith('due:'):
176
            self.due = self._parseDateTime(word[4:])
177
            if not self.due:
178
                print("Error parsing due date '{}'".format(word))
179
                self.due_error = word[4:]
180
        elif word.startswith('t:'):
181
            self._parseFuture(word)
182
        elif word.startswith('rec:'):
183
            self._parseRecurrence(word)
184
185
    def _parseFuture(self, word):
186
        self.threshold = self._parseDateTime(word[2:])
187
        if not self.threshold:
188
            print("Error parsing threshold '{}'".format(word))
189
            self.threshold_error = word[2:]
190
        else:
191
            if self.threshold > datetime.today():
192
                self.is_future = True
193
194
    def _parseRecurrence(self, word):
195
        # Original due date mode
196
        if word[4] == '+':
197
            # Test if chracters have the right format
198
            if re.match('^[1-9][bdwmy]', word[5:7]):
199
                self.recursion = recursion(recursiveMode.originalDueDate, word[5], word[6])
200
            else:
201
                print("Error parsing recurrence '{}'".format(word))
202
        # Completion mode
203
        else:
204
            # Test if chracters have the right format
205
            if re.match('^[1-9][bdwmy]', word[4:6]):
206
                self.recursion = recursion(recursiveMode.completitionDate, word[4], word[5])
207
            else:
208
                print("Error parsing recurrence '{}'".format(word))
209
210
    def _parseDate(self, string):
211
        try:
212
            return datetime.strptime(string, '%Y-%m-%d').date()
213
        except ValueError:
214
            return None
215
216
    def _parseDateTime(self, string):
217
        try:
218
            return datetime.strptime(string, '%Y-%m-%d')
219
        except ValueError:
220
            try:
221
                return datetime.strptime(string, '%Y-%m-%dT%H:%M')
222
            except ValueError:
223
                return None
224
225
    @property
226
    def dueString(self):
227
        return dateString(self.due)
228
229
    @staticmethod
230
    def updateDateInTask(text, newDate):
231
        # FIXME: This method has nothing to do in this class, move womewhere else
232
        # (A) 2016-12-08 Feed Schrodinger's Cat rec:9w due:2016-11-23
233
        text = re.sub('\sdue\:[0-9]{4}\-[0-9]{2}\-[0-9]{2}', ' due:' + str(newDate)[0:10], text)
234
        return text
235
236
    @property
237
    def thresholdString(self):
238
        return dateString(self.threshold)
239
240
    @QtCore.pyqtSlot()
241
    def toggleCompletion(self):
242
        if self.is_complete:
243
            self.setPending()
244
        else:
245
            self.setCompleted()
246
247
    def setCompleted(self):
248
        """
249
        Set a task as completed by inserting a x and current date at the begynning of line
250
        """
251
        if self.is_complete:
252
            return
253
        self.completion_date = date.today()
254
        date_string = self.completion_date.strftime('%Y-%m-%d')
255
        self._text = 'x %s %s' % (date_string, self._text)
256
        self.is_complete = True
257
        self.modified.emit(self)
258
259
    def setPending(self):
260
        """
261
        Unset completed flag from task
262
        """
263
        if not self.is_complete:
264
            return
265
        words = self._text.split(" ")
266
        d = self._parseDate(words[1])
267
        if d:
268
            self._text = " ".join(words[2:])
269
        else:
270
            self._text = " ".join(words[1:])
271
        self.is_complete = False
272
        self.completion_date = None
273
        self.modified.emit(self)
274
275
    def toHtml(self):
276
        """
277
        return a task as an html block which is a pretty display of a line in todo.txt format
278
        """
279
        htmlizer = TaskHtmlizer()
280
        return htmlizer.task2html(self)
281
282
    def _getLowestPriority(self):
283
        return self._settings.value("Preferences/lowest_priority", "D")
284
285
    @QtCore.pyqtSlot()
286
    def increasePriority(self):
287
        lowest_priority = self._getLowestPriority()
288
        if self.is_complete:
289
            return
290
        if not self._priority:
291
            self._priority = lowest_priority
292
            self._text = "({}) {}".format(self._priority, self._text)
293
        elif self._priority != self._highest_priority:
294
            self._priority = chr(ord(self._priority) - 1)
295
            self._text = "({}) {}".format(self._priority, self._text[4:])
296
        self.modified.emit(self)
297
298
    @QtCore.pyqtSlot()
299
    def decreasePriority(self):
300
        lowest_priority = self._getLowestPriority()
301
        if self.is_complete:
302
            return
303
        if self._priority >= lowest_priority:
304
            self._priority = ""
305
            self._text = self._text[4:]
306
            self._text = self._text.replace("({})".format(self._priority), "", 1)
307
        elif self._priority:
308
            oldpriority = self._priority
309
            self._priority = chr(ord(self._priority) + 1)
310
            self._text = self._text.replace("({})".format(oldpriority), "({})".format(self._priority), 1)
311
        self.modified.emit(self)
312
313
    def __eq__(self, other):
314
        return self._text == other.text
315
316
    def __lt__(self, other):
317
        if self.is_complete != other.is_complete:
318
            return self._lowerCompleteness(other)
319
        if self._priority != other.priority:
320
            if not self._priority:
321
                return False
322
            if not other.priority:
323
                return True
324
            return self._priority < other.priority
325
        # order the other tasks alphabetically
326
        return self._text < other.text
327
328
    def _lowerCompleteness(self, other):
329
        if self.is_complete and not other.is_complete:
330
            return False
331
        if not self.is_complete and other.is_complete:
332
            return True
333
        raise RuntimeError("Could not compare completeness of 2 tasks, please report")
334
335
336
def dateString(date):
337
    """
338
    Return a datetime as a nicely formatted string
339
    """
340
    if date.time() == time.min:
341
        return date.strftime('%Y-%m-%d')
342
    else:
343
        return date.strftime('%Y-%m-%d %H:%M')
344
345
346
def filterTasks(filters, tasks):
347
    if not filters:
348
        return tasks[:]
349
350
    filteredTasks = []
351
    for task in tasks:
352
        for myfilter in filters:
353
            if myfilter.isMatch(task):
354
                filteredTasks.append(task)
355
                break
356
    return filteredTasks
357