Completed
Push — qml ( 5583cc...9e7fdd )
by Olivier
01:04
created

Task.text()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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