Completed
Push — qml ( 3ae19f...0f9f4b )
by Olivier
01:02
created

Task._parse()   B

Complexity

Conditions 6

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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