Completed
Push — qml ( 4ca48a...8e2761 )
by Olivier
01:05
created

recurTask()   F

Complexity

Conditions 11

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
c 0
b 0
f 0
dl 0
loc 35
rs 3.1764

How to fix   Complexity   

Complexity

Complex classes like recurTask() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from datetime import datetime, date, time, MAXYEAR, timedelta
2
import re
3
from enum import Enum
4
5
from PyQt5 import QtCore
6
7
from qtodotxt2.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 = _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 = _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 = _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 = _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
    @property
214
    def dueString(self):
215
        return dateString(self.due)
216
217
    @property
218
    def thresholdString(self):
219
        return dateString(self.threshold)
220
221
    @QtCore.pyqtSlot()
222
    def toggleCompletion(self):
223
        if self.is_complete:
224
            self.setPending()
225
        else:
226
            self.setCompleted()
227
228
    def setCompleted(self):
229
        """
230
        Set a task as completed by inserting a x and current date at the begynning of line
231
        """
232
        if self.is_complete:
233
            return
234
        self.completion_date = date.today()
235
        date_string = self.completion_date.strftime('%Y-%m-%d')
236
        self._text = 'x %s %s' % (date_string, self._text)
237
        self.is_complete = True
238
        self.modified.emit(self)
239
240
    def setPending(self):
241
        """
242
        Unset completed flag from task
243
        """
244
        if not self.is_complete:
245
            return
246
        words = self._text.split(" ")
247
        d = _parseDate(words[1])
248
        if d:
249
            self._text = " ".join(words[2:])
250
        else:
251
            self._text = " ".join(words[1:])
252
        self.is_complete = False
253
        self.completion_date = None
254
        self.modified.emit(self)
255
256
    def toHtml(self):
257
        """
258
        return a task as an html block which is a pretty display of a line in todo.txt format
259
        """
260
        htmlizer = TaskHtmlizer()
261
        return htmlizer.task2html(self)
262
263
    def _getLowestPriority(self):
264
        return self._settings.value("Preferences/lowest_priority", "D")
265
266
    @QtCore.pyqtSlot()
267
    def increasePriority(self):
268
        lowest_priority = self._getLowestPriority()
269
        if self.is_complete:
270
            return
271
        if not self._priority:
272
            self._priority = lowest_priority
273
            self._text = "({}) {}".format(self._priority, self._text)
274
        elif self._priority != self._highest_priority:
275
            self._priority = chr(ord(self._priority) - 1)
276
            self._text = "({}) {}".format(self._priority, self._text[4:])
277
        self.modified.emit(self)
278
279
    @QtCore.pyqtSlot()
280
    def decreasePriority(self):
281
        lowest_priority = self._getLowestPriority()
282
        if self.is_complete:
283
            return
284
        if self._priority >= lowest_priority:
285
            self._priority = ""
286
            self._text = self._text[4:]
287
            self._text = self._text.replace("({})".format(self._priority), "", 1)
288
        elif self._priority:
289
            oldpriority = self._priority
290
            self._priority = chr(ord(self._priority) + 1)
291
            self._text = self._text.replace("({})".format(oldpriority), "({})".format(self._priority), 1)
292
        self.modified.emit(self)
293
294
    def __eq__(self, other):
295
        return self._text == other.text
296
297
    def __lt__(self, other):
298
        prio1 = self.priority if self.priority else "z"
299
        prio2 = other.priority if other.priority else "z"
300
        return (self.is_complete, prio1, self._text) < (other.is_complete, prio2, other.text)
301
302
303
def dateString(dato):
304
    """
305
    Return a datetime as a nicely formatted string
306
    """
307
    if dato.time() == time.min:
308
        return dato.strftime('%Y-%m-%d')
309
    else:
310
        return dato.strftime('%Y-%m-%d %H:%M')
311
312
313
def _incrWorkDays(startDate, daysToIncrement):
314
    while daysToIncrement > 0:
315
        if startDate.weekday() == 4:  # Friday
316
            startDate = startDate + timedelta(days=3)
317
        elif startDate.weekday() == 5:  # Saturday
318
            startDate = startDate + timedelta(days=2)
319
        else:
320
            startDate = startDate + timedelta(days=1)
321
        daysToIncrement -= 1
322
    return startDate
323
324
325
def recurTask(task):
326
    """
327
    Create the next task from a recurring task
328
    """
329
    if task.recursion.interval == 'd':
330
        if task.recursion.mode == RecursiveMode.originalDueDate:
331
            next_due_date = task.due + timedelta(days=int(task.recursion.increment))
332
        else:
333
            next_due_date = date.today() + timedelta(days=int(task.recursion.increment))
334
    elif task.recursion.interval == 'b':
335
        if task.recursion.mode == RecursiveMode.originalDueDate:
336
            next_due_date = _incrWorkDays(task.due, int(task.recursion.increment))
337
        else:
338
            next_due_date = _incrWorkDays(date.today(), int(task.recursion.increment))
339
    elif task.recursion.interval == 'w':
340
        if task.recursion.mode == RecursiveMode.originalDueDate:
341
            next_due_date = task.due + timedelta(weeks=int(task.recursion.increment))
342
        else:
343
            next_due_date = date.today() + timedelta(weeks=int(task.recursion.increment))
344
    elif task.recursion.interval == 'm':
345
        if task.recursion.mode == RecursiveMode.originalDueDate:
346
            next_due_date = task.due + timedelta(weeks=int(task.recursion.increment) * 4)  # 4 weeks in a month
347
        else:
348
            next_due_date = date.today() + timedelta(weeks=int(task.recursion.increment) * 4)  # 4 weeks in a month
349
    elif task.recursion.interval == 'y':
350
        if task.recursion.mode == RecursiveMode.originalDueDate:
351
            next_due_date = task.due + timedelta(weeks=int(task.recursion.increment) * 52)  # 52 weeks in a year
352
        else:
353
            next_due_date = date.today() + timedelta(weeks=int(task.recursion.increment) * 52)  # 52 weeks in a year
354
    else:
355
        # Test already made during line parsing - shouldn't be a problem here
356
        pass
357
    # Set new due date in old task text
358
    text = re.sub(r'\sdue\:[0-9]{4}\-[0-9]{2}\-[0-9]{2}', ' due:' + dateString(next_due_date)[0:10], task.text)
359
    return Task(text)
360
361
362
def _parseDate(string):
363
    try:
364
        return datetime.strptime(string, '%Y-%m-%d').date()
365
    except ValueError:
366
        return None
367
368
369
def _parseDateTime(string):
370
    try:
371
        return datetime.strptime(string, '%Y-%m-%d')
372
    except ValueError:
373
        try:
374
            return datetime.strptime(string, '%Y-%m-%dT%H:%M')
375
        except ValueError:
376
            return None
377
378
379