Merge lp:~gtg-contributors/gtg/new-date-class into lp:~gtg/gtg/old-trunk
- new-date-class
- Merge into old-trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Izidor Matušov | ||||
Approved revision: | 823 | ||||
Merged at revision: | 1089 | ||||
Proposed branch: | lp:~gtg-contributors/gtg/new-date-class | ||||
Merge into: | lp:~gtg/gtg/old-trunk | ||||
Diff against target: |
959 lines (+300/-282) 10 files modified
GTG/core/filters_bank.py (+6/-6) GTG/core/requester.py (+0/-1) GTG/core/task.py (+17/-20) GTG/gtk/browser/browser.py (+9/-13) GTG/gtk/dbuswrapper.py (+8/-8) GTG/gtk/editor/editor.py (+24/-26) GTG/plugins/evolution_sync/gtgTask.py (+6/-5) GTG/plugins/rtm_sync/gtgTask.py (+6/-5) GTG/tools/dates.py (+216/-190) GTG/tools/taskxml.py (+8/-8) |
||||
To merge this branch: | bzr merge lp:~gtg-contributors/gtg/new-date-class | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Natsuo Kishimoto (community) | Approve | ||
Bryce Harrington (community) | code | Approve | |
Review via email: mp+28009@code.launchpad.net |
Commit message
Description of the change
This branch almost entirely rewrites the various classes that were contained in GTG/tools/dates.py.
When the GTG UI and backend are separated over DBus, Python objects (including built-in and custom dates) cannot be passed directly. Strings can be used instead. The new Date class in this code is designed to always obey:
Date(str(d)) = d
for any Date instance d, even for special dates ('soon', 'later', etc.). As a result, neither client nor server code need make any distinction between FuzzyDates or RealDates or so on; it can simply construct Date() with the information passed over DBus.
The class also follows some of the semantics from the Python datetime module; for example:
d1 = datetime.
d2 = Date.soon() # get a Date instance representing the special date 'soon'
I have tested the branch in several ways, but some additional experimentation would be appreciated to see if any bugs were introduced.
Luca Invernizzi (invernizzi) wrote : | # |
Paul Natsuo Kishimoto (khaeru) wrote : | # |
I had — pickling is fast, but the output isn't necessarily easy to parse for non-Python code that might be interacting with the DBus API.
Luca Invernizzi (invernizzi) wrote : | # |
FYI, I found out that in current trunk we have that:
string = str(get_
That would let us keep the subclasses about fuzzy dates, which could be useful if we decide to expand our "fuzzyness" support.
Paul Natsuo Kishimoto (khaeru) wrote : | # |
I tried to make Date.parse(...) a replacement for get_canonical_
The difference between Date.parse and Date.__init__ is that parse() checks a lot more possible formats, while __init__() expects simpler input and also recognizes various objects like datetime. __init__() is more for internal use (hopefully also faster, though I haven't checked...)
To handle more formats (e.g. alien ones belonging to various remote backends), I would extend parse() and keep __init__() as-is.
They are about equivalent, but I think it is easier to add more constants like TODAY, SOON, etc. than to subclass Date. This way, any new fuzzy dates will have to behave in a way that is consistent with the existing ones.
Bryce Harrington (bryce) wrote : | # |
Looks good. I like the elimination of difference between RealDate and FuzzyDate.
I can't think of any other fuzzy date terms that would be worth adding, but I'm sure someone will think of one or two, so being able to extend that would be a good thing.
Paul Natsuo Kishimoto (khaeru) wrote : | # |
Anybody in gtg-devs feel like committing this? :)
Paul Natsuo Kishimoto (khaeru) wrote : | # |
Once again, can this be either committed or rejected?
Paul Natsuo Kishimoto (khaeru) : | # |
Preview Diff
1 | === modified file 'GTG/core/filters_bank.py' |
2 | --- GTG/core/filters_bank.py 2010-06-14 19:30:50 +0000 |
3 | +++ GTG/core/filters_bank.py 2010-06-20 04:28:25 +0000 |
4 | @@ -23,8 +23,8 @@ |
5 | |
6 | from datetime import datetime |
7 | |
8 | -from GTG.core.task import Task |
9 | -from GTG.tools.dates import date_today, no_date, Date |
10 | +from GTG.core.task import Task |
11 | +from GTG.tools.dates import * |
12 | |
13 | |
14 | class Filter: |
15 | @@ -179,12 +179,12 @@ |
16 | def is_started(self,task,parameters=None): |
17 | '''Filter for tasks that are already started''' |
18 | start_date = task.get_start_date() |
19 | - if start_date : |
20 | + if start_date: |
21 | #Seems like pylint falsely assumes that subtraction always results |
22 | #in an object of the same type. The subtraction of dates |
23 | #results in a datetime.timedelta object |
24 | #that does have a 'days' member. |
25 | - difference = date_today() - start_date |
26 | + difference = Date.today() - start_date |
27 | if difference.days == 0: |
28 | # Don't count today's tasks started until morning |
29 | return datetime.now().hour > 4 |
30 | @@ -202,14 +202,14 @@ |
31 | def workdue(self,task): |
32 | ''' Filter for tasks due within the next day ''' |
33 | wv = self.workview(task) and \ |
34 | - task.get_due_date() != no_date and \ |
35 | + task.get_due_date() != Date.no_date() and \ |
36 | task.get_days_left() < 2 |
37 | return wv |
38 | |
39 | def worklate(self,task): |
40 | ''' Filter for tasks due within the next day ''' |
41 | wv = self.workview(task) and \ |
42 | - task.get_due_date() != no_date and \ |
43 | + task.get_due_date() != Date.no_date() and \ |
44 | task.get_days_late() > 0 |
45 | return wv |
46 | |
47 | |
48 | === modified file 'GTG/core/requester.py' |
49 | --- GTG/core/requester.py 2010-06-12 13:59:24 +0000 |
50 | +++ GTG/core/requester.py 2010-06-20 04:28:25 +0000 |
51 | @@ -27,7 +27,6 @@ |
52 | from GTG.core.filters_bank import FiltersBank |
53 | from GTG.core.task import Task |
54 | from GTG.core.tagstore import Tag |
55 | -from GTG.tools.dates import date_today |
56 | from GTG.tools.logger import Log |
57 | |
58 | class Requester(gobject.GObject): |
59 | |
60 | === modified file 'GTG/core/task.py' |
61 | --- GTG/core/task.py 2010-06-18 16:36:17 +0000 |
62 | +++ GTG/core/task.py 2010-06-20 04:28:25 +0000 |
63 | @@ -20,15 +20,15 @@ |
64 | """ |
65 | task.py contains the Task class which represents (guess what) a task |
66 | """ |
67 | - |
68 | +import cgi |
69 | +from datetime import datetime |
70 | +import uuid |
71 | import xml.dom.minidom |
72 | -import uuid |
73 | -import cgi |
74 | import xml.sax.saxutils as saxutils |
75 | |
76 | from GTG import _ |
77 | -from GTG.tools.dates import date_today, no_date, Date |
78 | -from datetime import datetime |
79 | +from GTG.tools.dates import * |
80 | + |
81 | from GTG.core.tree import TreeNode |
82 | from GTG.tools.logger import Log |
83 | |
84 | @@ -55,9 +55,9 @@ |
85 | self.title = _("My new task") |
86 | #available status are: Active - Done - Dismiss - Note |
87 | self.status = self.STA_ACTIVE |
88 | - self.closed_date = no_date |
89 | - self.due_date = no_date |
90 | - self.start_date = no_date |
91 | + self.closed_date = Date.no_date() |
92 | + self.due_date = Date.no_date() |
93 | + self.start_date = Date.no_date() |
94 | self.can_be_deleted = newtask |
95 | # tags |
96 | self.tags = [] |
97 | @@ -134,10 +134,10 @@ |
98 | c.set_status(status, donedate=donedate) |
99 | #to the specified date (if any) |
100 | if donedate: |
101 | - self.closed_date = donedate |
102 | + self.closed_date = Date(donedate) |
103 | #or to today |
104 | else: |
105 | - self.closed_date = date_today() |
106 | + self.closed_date = Date.today() |
107 | #If we mark a task as Active and that some parent are not |
108 | #Active, we break the parent/child relation |
109 | #It has no sense to have an active subtask of a done parent. |
110 | @@ -166,14 +166,13 @@ |
111 | return self.modified |
112 | |
113 | def get_modified_string(self): |
114 | - return self.modified.strftime("%Y-%m-%dT%H:%M:%S") |
115 | + return self.modified.isoformat() |
116 | |
117 | def set_modified(self, modified): |
118 | self.modified = modified |
119 | |
120 | def set_due_date(self, fulldate): |
121 | - assert(isinstance(fulldate, Date)) |
122 | - self.due_date = fulldate |
123 | + self.due_date = Date(fulldate) |
124 | self.sync() |
125 | |
126 | #Due date return the most urgent date of all parents |
127 | @@ -189,16 +188,14 @@ |
128 | return zedate |
129 | |
130 | def set_start_date(self, fulldate): |
131 | - assert(isinstance(fulldate, Date)) |
132 | - self.start_date = fulldate |
133 | + self.start_date = Date(fulldate) |
134 | self.sync() |
135 | |
136 | def get_start_date(self): |
137 | return self.start_date |
138 | |
139 | def set_closed_date(self, fulldate): |
140 | - assert(isinstance(fulldate, Date)) |
141 | - self.closed_date = fulldate |
142 | + self.closed_date = Date(fulldate) |
143 | self.sync() |
144 | |
145 | def get_closed_date(self): |
146 | @@ -206,13 +203,13 @@ |
147 | |
148 | def get_days_left(self): |
149 | due_date = self.get_due_date() |
150 | - if due_date == no_date: |
151 | + if due_date == Date.no_date(): |
152 | return None |
153 | - return due_date.days_left() |
154 | + return (due_date - Date.today()).days |
155 | |
156 | def get_days_late(self): |
157 | due_date = self.get_due_date() |
158 | - if due_date == no_date: |
159 | + if due_date == Date.no_date(): |
160 | return None |
161 | closed_date = self.get_closed_date() |
162 | return (closed_date - due_date).days |
163 | |
164 | === modified file 'GTG/gtk/browser/browser.py' |
165 | --- GTG/gtk/browser/browser.py 2010-06-17 08:58:32 +0000 |
166 | +++ GTG/gtk/browser/browser.py 2010-06-20 04:28:25 +0000 |
167 | @@ -45,10 +45,7 @@ |
168 | ClosedTaskTreeView |
169 | from GTG.gtk.browser.tagtree import TagTree |
170 | from GTG.tools import openurl |
171 | -from GTG.tools.dates import strtodate,\ |
172 | - no_date,\ |
173 | - FuzzyDate, \ |
174 | - get_canonical_date |
175 | +from GTG.tools.dates import * |
176 | from GTG.tools.logger import Log |
177 | #from GTG.tools import clipboard |
178 | |
179 | @@ -607,13 +604,12 @@ |
180 | return s |
181 | else: |
182 | return -1 * s |
183 | - |
184 | - |
185 | + |
186 | if sort == 0: |
187 | # Put fuzzy dates below real dates |
188 | - if isinstance(t1, FuzzyDate) and not isinstance(t2, FuzzyDate): |
189 | + if t1.is_special and not t2.is_special: |
190 | sort = reverse_if_descending(1) |
191 | - elif isinstance(t2, FuzzyDate) and not isinstance(t1, FuzzyDate): |
192 | + elif t2.is_special and not t1.is_special: |
193 | sort = reverse_if_descending(-1) |
194 | |
195 | if sort == 0: # Group tasks with the same tag together for visual cleanness |
196 | @@ -915,8 +911,8 @@ |
197 | |
198 | def on_quickadd_activate(self, widget): |
199 | text = self.quickadd_entry.get_text() |
200 | - due_date = no_date |
201 | - defer_date = no_date |
202 | + due_date = Date.no_date() |
203 | + defer_date = Date.no_date() |
204 | if text: |
205 | tags, notagonly = self.get_selected_tags() |
206 | # Get tags in the title |
207 | @@ -940,12 +936,12 @@ |
208 | tags.append(GTG.core.tagstore.Tag(tag, self.req)) |
209 | elif attribute.lower() == "defer" or \ |
210 | attribute.lower() == _("defer"): |
211 | - defer_date = get_canonical_date(args) |
212 | + defer_date = Date.parse(args) |
213 | if not defer_date: |
214 | valid_attribute = False |
215 | elif attribute.lower() == "due" or \ |
216 | attribute.lower() == _("due"): |
217 | - due_date = get_canonical_date(args) |
218 | + due_date = Date.parse(args) |
219 | if not due_date: |
220 | valid_attribute = False |
221 | else: |
222 | @@ -1114,7 +1110,7 @@ |
223 | tasks = [self.req.get_task(uid) for uid in tasks_uid] |
224 | tasks_status = [task.get_status() for task in tasks] |
225 | for uid, task, status in zip(tasks_uid, tasks, tasks_status): |
226 | - task.set_start_date(get_canonical_date(new_start_date)) |
227 | + task.set_start_date(Date.parse(new_start_date)) |
228 | #FIXME: If the task dialog is displayed, refresh its start_date widget |
229 | |
230 | def on_mark_as_started(self, widget): |
231 | |
232 | === modified file 'GTG/gtk/dbuswrapper.py' |
233 | --- GTG/gtk/dbuswrapper.py 2010-06-10 14:45:36 +0000 |
234 | +++ GTG/gtk/dbuswrapper.py 2010-06-20 04:28:25 +0000 |
235 | @@ -23,8 +23,8 @@ |
236 | import dbus.glib |
237 | import dbus.service |
238 | |
239 | -from GTG.core import CoreConfig |
240 | -from GTG.tools import dates |
241 | +from GTG.core import CoreConfig |
242 | +from GTG.tools.dates import * |
243 | |
244 | |
245 | BUSNAME = CoreConfig.BUSNAME |
246 | @@ -169,10 +169,10 @@ |
247 | nt = self.req.new_task(tags=tags) |
248 | for sub in subtasks: |
249 | nt.add_child(sub) |
250 | - nt.set_status(status, donedate=dates.strtodate(donedate)) |
251 | + nt.set_status(status, donedate=Date.parse(donedate)) |
252 | nt.set_title(title) |
253 | - nt.set_due_date(dates.strtodate(duedate)) |
254 | - nt.set_start_date(dates.strtodate(startdate)) |
255 | + nt.set_due_date(Date.parse(duedate)) |
256 | + nt.set_start_date(Date.parse(startdate)) |
257 | nt.set_text(text) |
258 | return task_to_dict(nt) |
259 | |
260 | @@ -187,10 +187,10 @@ |
261 | via this function. |
262 | """ |
263 | task = self.req.get_task(tid) |
264 | - task.set_status(task_data["status"], donedate=dates.strtodate(task_data["donedate"])) |
265 | + task.set_status(task_data["status"], donedate=Date.parse(task_data["donedate"])) |
266 | task.set_title(task_data["title"]) |
267 | - task.set_due_date(dates.strtodate(task_data["duedate"])) |
268 | - task.set_start_date(dates.strtodate(task_data["startdate"])) |
269 | + task.set_due_date(Date.parse(task_data["duedate"])) |
270 | + task.set_start_date(Date.parse(task_data["startdate"])) |
271 | task.set_text(task_data["text"]) |
272 | |
273 | for tag in task_data["tags"]: |
274 | |
275 | === modified file 'GTG/gtk/editor/editor.py' |
276 | --- GTG/gtk/editor/editor.py 2010-06-07 21:14:45 +0000 |
277 | +++ GTG/gtk/editor/editor.py 2010-06-20 04:28:25 +0000 |
278 | @@ -45,7 +45,7 @@ |
279 | from GTG.core.plugins.engine import PluginEngine |
280 | from GTG.core.plugins.api import PluginAPI |
281 | from GTG.core.task import Task |
282 | -from GTG.tools import dates |
283 | +from GTG.tools.dates import * |
284 | |
285 | |
286 | date_separator = "-" |
287 | @@ -307,13 +307,13 @@ |
288 | |
289 | #refreshing the due date field |
290 | duedate = self.task.get_due_date() |
291 | - prevdate = dates.strtodate(self.duedate_widget.get_text()) |
292 | + prevdate = Date.parse(self.duedate_widget.get_text()) |
293 | if duedate != prevdate or type(duedate) is not type(prevdate): |
294 | zedate = str(duedate).replace("-", date_separator) |
295 | self.duedate_widget.set_text(zedate) |
296 | # refreshing the closed date field |
297 | closeddate = self.task.get_closed_date() |
298 | - prevcldate = dates.strtodate(self.closeddate_widget.get_text()) |
299 | + prevcldate = Date.parse(self.closeddate_widget.get_text()) |
300 | if closeddate != prevcldate or type(closeddate) is not type(prevcldate): |
301 | zecldate = str(closeddate).replace("-", date_separator) |
302 | self.closeddate_widget.set_text(zecldate) |
303 | @@ -348,7 +348,7 @@ |
304 | self.dayleft_label.set_markup("<span color='"+color+"'>"+txt+"</span>") |
305 | |
306 | startdate = self.task.get_start_date() |
307 | - prevdate = dates.strtodate(self.startdate_widget.get_text()) |
308 | + prevdate = Date.parse(self.startdate_widget.get_text()) |
309 | if startdate != prevdate or type(startdate) is not type(prevdate): |
310 | zedate = str(startdate).replace("-",date_separator) |
311 | self.startdate_widget.set_text(zedate) |
312 | @@ -377,9 +377,9 @@ |
313 | validdate = False |
314 | if not text : |
315 | validdate = True |
316 | - datetoset = dates.no_date |
317 | + datetoset = Date.no_date() |
318 | else : |
319 | - datetoset = dates.strtodate(text) |
320 | + datetoset = Date.parse(text) |
321 | if datetoset : |
322 | validdate = True |
323 | |
324 | @@ -400,15 +400,15 @@ |
325 | widget.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F88")) |
326 | |
327 | def _mark_today_in_bold(self): |
328 | - today = dates.date_today() |
329 | + today = Date.today() |
330 | #selected is a tuple containing (year, month, day) |
331 | selected = self.cal_widget.get_date() |
332 | #the following "-1" is because in pygtk calendar the month is 0-based, |
333 | # in gtg (and datetime.date) is 1-based. |
334 | - if selected[1] == today.month() - 1 and selected[0] == today.year(): |
335 | - self.cal_widget.mark_day(today.day()) |
336 | + if selected[1] == today.month - 1 and selected[0] == today.year: |
337 | + self.cal_widget.mark_day(today.day) |
338 | else: |
339 | - self.cal_widget.unmark_day(today.day()) |
340 | + self.cal_widget.unmark_day(today.day) |
341 | |
342 | |
343 | def on_date_pressed(self, widget,data): |
344 | @@ -440,15 +440,13 @@ |
345 | gdk.pointer_grab(self.calendar.window, True,gdk.BUTTON1_MASK|gdk.MOD2_MASK) |
346 | #we will close the calendar if the user clicks outside |
347 | |
348 | - if not isinstance(toset, dates.FuzzyDate): |
349 | - if not toset: |
350 | - # we set the widget to today's date if there is not a date defined |
351 | - toset = dates.date_today() |
352 | - |
353 | - y = toset.year() |
354 | - m = toset.month() |
355 | - d = int(toset.day()) |
356 | - |
357 | + if not toset: |
358 | + # we set the widget to today's date if there is not a date defined |
359 | + toset = Date.today() |
360 | + elif not toset.is_special: |
361 | + y = toset.year |
362 | + m = toset.month |
363 | + d = int(toset.day) |
364 | #We have to select the day first. If not, we might ask for |
365 | #February while still being on 31 -> error ! |
366 | self.cal_widget.select_day(d) |
367 | @@ -463,11 +461,11 @@ |
368 | def day_selected(self,widget) : |
369 | y,m,d = widget.get_date() |
370 | if self.__opened_date == "due" : |
371 | - self.task.set_due_date(dates.strtodate("%s-%s-%s"%(y,m+1,d))) |
372 | + self.task.set_due_date(Date.parse("%s-%s-%s"%(y,m+1,d))) |
373 | elif self.__opened_date == "start" : |
374 | - self.task.set_start_date(dates.strtodate("%s-%s-%s"%(y,m+1,d))) |
375 | + self.task.set_start_date(Date.parse("%s-%s-%s"%(y,m+1,d))) |
376 | elif self.__opened_date == "closed" : |
377 | - self.task.set_closed_date(dates.strtodate("%s-%s-%s"%(y,m+1,d))) |
378 | + self.task.set_closed_date(Date.parse("%s-%s-%s"%(y,m+1,d))) |
379 | if self.close_when_changed : |
380 | #When we select a day, we connect the mouse release to the |
381 | #closing of the calendar. |
382 | @@ -497,16 +495,16 @@ |
383 | self.__close_calendar() |
384 | |
385 | def nodate_pressed(self,widget) : #pylint: disable-msg=W0613 |
386 | - self.set_opened_date(dates.no_date) |
387 | + self.set_opened_date(Date.no_date()) |
388 | |
389 | def set_fuzzydate_now(self, widget) : #pylint: disable-msg=W0613 |
390 | - self.set_opened_date(dates.NOW) |
391 | + self.set_opened_date(Date.today()) |
392 | |
393 | def set_fuzzydate_soon(self, widget) : #pylint: disable-msg=W0613 |
394 | - self.set_opened_date(dates.SOON) |
395 | + self.set_opened_date(Date.soon()) |
396 | |
397 | def set_fuzzydate_later(self, widget) : #pylint: disable-msg=W0613 |
398 | - self.set_opened_date(dates.LATER) |
399 | + self.set_opened_date(Date.later()) |
400 | |
401 | def dismiss(self,widget) : #pylint: disable-msg=W0613 |
402 | stat = self.task.get_status() |
403 | |
404 | === modified file 'GTG/plugins/evolution_sync/gtgTask.py' |
405 | --- GTG/plugins/evolution_sync/gtgTask.py 2010-03-16 02:16:14 +0000 |
406 | +++ GTG/plugins/evolution_sync/gtgTask.py 2010-06-20 04:28:25 +0000 |
407 | @@ -16,9 +16,10 @@ |
408 | |
409 | import datetime |
410 | |
411 | -from GTG.tools.dates import NoDate, RealDate |
412 | +from GTG.tools.dates import * |
413 | from GTG.plugins.evolution_sync.genericTask import GenericTask |
414 | |
415 | + |
416 | class GtgTask(GenericTask): |
417 | |
418 | def __init__(self, gtg_task, plugin_api, gtg_proxy): |
419 | @@ -62,15 +63,15 @@ |
420 | |
421 | def _get_due_date(self): |
422 | due_date = self._gtg_task.get_due_date() |
423 | - if due_date == NoDate(): |
424 | + if due_date == Date.no_date(): |
425 | return None |
426 | - return due_date.to_py_date() |
427 | + return due_date._date |
428 | |
429 | def _set_due_date(self, due): |
430 | if due == None: |
431 | - gtg_due = NoDate() |
432 | + gtg_due = Date.no_date() |
433 | else: |
434 | - gtg_due = RealDate(due) |
435 | + gtg_due = Date(due) |
436 | self._gtg_task.set_due_date(gtg_due) |
437 | |
438 | def _get_modified(self): |
439 | |
440 | === modified file 'GTG/plugins/rtm_sync/gtgTask.py' |
441 | --- GTG/plugins/rtm_sync/gtgTask.py 2010-05-05 21:54:17 +0000 |
442 | +++ GTG/plugins/rtm_sync/gtgTask.py 2010-06-20 04:28:25 +0000 |
443 | @@ -16,9 +16,10 @@ |
444 | |
445 | import datetime |
446 | |
447 | -from GTG.tools.dates import NoDate, RealDate |
448 | +from GTG.tools.dates import * |
449 | from GTG.plugins.rtm_sync.genericTask import GenericTask |
450 | |
451 | + |
452 | class GtgTask(GenericTask): |
453 | #GtgTask passes only datetime objects with the timezone loaded |
454 | # to talk about dates and times |
455 | @@ -76,15 +77,15 @@ |
456 | |
457 | def _get_due_date(self): |
458 | due_date = self._gtg_task.get_due_date() |
459 | - if due_date == NoDate(): |
460 | + if due_date == Date.no_date(): |
461 | return None |
462 | - return due_date.to_py_date() |
463 | + return due_date._date |
464 | |
465 | def _set_due_date(self, due): |
466 | if due == None: |
467 | - gtg_due = NoDate() |
468 | + gtg_due = Date.no_date() |
469 | else: |
470 | - gtg_due = RealDate(due) |
471 | + gtg_due = Date(due) |
472 | self._gtg_task.set_due_date(gtg_due) |
473 | |
474 | def _get_modified(self): |
475 | |
476 | === modified file 'GTG/tools/dates.py' |
477 | --- GTG/tools/dates.py 2010-04-30 19:23:02 +0000 |
478 | +++ GTG/tools/dates.py 2010-06-20 04:28:25 +0000 |
479 | @@ -17,214 +17,240 @@ |
480 | # this program. If not, see <http://www.gnu.org/licenses/>. |
481 | # ----------------------------------------------------------------------------- |
482 | |
483 | -from datetime import date, timedelta |
484 | +import calendar |
485 | +import datetime |
486 | import locale |
487 | -import calendar |
488 | from GTG import _, ngettext |
489 | |
490 | -#setting the locale of gtg to the system locale |
491 | -#locale.setlocale(locale.LC_TIME, '') |
492 | + |
493 | +__all__ = 'Date', |
494 | + |
495 | + |
496 | +## internal constants |
497 | +# integers for special dates |
498 | +TODAY, SOON, NODATE, LATER = range(4) |
499 | +# strings representing special dates |
500 | +STRINGS = { |
501 | + TODAY: 'today', |
502 | + SOON: 'soon', |
503 | + NODATE: '', |
504 | + LATER: 'later', |
505 | + } |
506 | +# inverse of STRINGS |
507 | +LOOKUP = dict([(v, k) for (k, v) in STRINGS.iteritems()]) |
508 | +# functions giving absolute dates for special dates |
509 | +FUNCS = { |
510 | + TODAY: lambda: datetime.date.today(), |
511 | + SOON: lambda: datetime.date.today() + datetime.timedelta(15), |
512 | + NODATE: lambda: datetime.date.max - datetime.timedelta(1), |
513 | + LATER: lambda: datetime.date.max, |
514 | + } |
515 | + |
516 | +# ISO 8601 date format |
517 | +ISODATE = '%Y-%m-%d' |
518 | + |
519 | + |
520 | +locale.setlocale(locale.LC_TIME, '') |
521 | + |
522 | |
523 | class Date(object): |
524 | + """A date class that supports fuzzy dates. |
525 | + |
526 | + Date supports all the methods of the standard datetime.date class. A Date |
527 | + can be constructed with: |
528 | + * the special strings 'today', 'soon', '' (no date, default), or 'later' |
529 | + * a string containing an ISO format date: YYYY-MM-DD, or |
530 | + * a datetime.date or Date instance. |
531 | + |
532 | + """ |
533 | + _date = None |
534 | + _special = None |
535 | + |
536 | + def __init__(self, value=''): |
537 | + if isinstance(value, datetime.date): |
538 | + self._date = value |
539 | + elif isinstance(value, Date): |
540 | + self._date = value._date |
541 | + self._special = value._special |
542 | + elif isinstance(value, str) or isinstance(value, unicode): |
543 | + try: # an ISO 8601 date |
544 | + self._date = datetime.datetime.strptime(value, ISODATE).date() |
545 | + except ValueError: |
546 | + try: # a special date |
547 | + self.__init__(LOOKUP[value]) |
548 | + except KeyError: |
549 | + raise ValueError |
550 | + elif isinstance(value, int): |
551 | + self._date = FUNCS[value]() |
552 | + self._special = value |
553 | + else: |
554 | + raise ValueError |
555 | + assert not (self._date is None and self._special is None) |
556 | + |
557 | + def __add__(self, other): |
558 | + """Addition, same usage as datetime.date.""" |
559 | + if isinstance(other, datetime.timedelta): |
560 | + return Date(self._date + other) |
561 | + else: |
562 | + raise NotImplementedError |
563 | + __radd__ = __add__ |
564 | + |
565 | + def __sub__(self, other): |
566 | + """Subtraction, same usage as datetime.date.""" |
567 | + if hasattr(other, '_date'): |
568 | + return self._date - other._date |
569 | + else: |
570 | + # if other is a datetime.date, this will work, otherwise let it |
571 | + # raise a NotImplementedError |
572 | + return self._date - other |
573 | + |
574 | + def __rsub__(self, other): |
575 | + """Subtraction, same usage as datetime.date.""" |
576 | + # opposite of __sub__ |
577 | + if hasattr(other, '_date'): |
578 | + return other._date - self._date |
579 | + else: |
580 | + return other - self._date |
581 | + |
582 | def __cmp__(self, other): |
583 | - if other is None: return 1 |
584 | - return cmp(self.to_py_date(), other.to_py_date()) |
585 | - |
586 | - def __sub__(self, other): |
587 | - return self.to_py_date() - other.to_py_date() |
588 | - |
589 | - def __get_locale_string(self): |
590 | - return locale.nl_langinfo(locale.D_FMT) |
591 | - |
592 | - def xml_str(self): return str(self) |
593 | - |
594 | - def day(self): return self.to_py_date().day |
595 | - def month(self): return self.to_py_date().month |
596 | - def year(self): return self.to_py_date().year |
597 | + """Compare with other Date instance.""" |
598 | + if hasattr(other, '_date'): |
599 | + return cmp(self._date, other._date) |
600 | + elif isinstance(other, datetime.date): |
601 | + return cmp(self._date, other) |
602 | + |
603 | + def __str__(self): |
604 | + """String representation. |
605 | + |
606 | + Date(str(d))) == d, always. |
607 | + |
608 | + """ |
609 | + if self._special: |
610 | + return STRINGS[self._special] |
611 | + else: |
612 | + return self._date.isoformat() |
613 | + |
614 | + def __getattr__(self, name): |
615 | + """Provide access to the wrapped datetime.date.""" |
616 | + try: |
617 | + return self.__dict__[name] |
618 | + except KeyError: |
619 | + return getattr(self._date, name) |
620 | + |
621 | + @property |
622 | + def is_special(self): |
623 | + """True if the Date is one of the special values; False if it is an |
624 | + absolute date.""" |
625 | + return not self._special |
626 | + |
627 | + @classmethod |
628 | + def today(cls): |
629 | + """Return the special Date 'today'.""" |
630 | + return Date(TODAY) |
631 | + |
632 | + @classmethod |
633 | + def no_date(cls): |
634 | + """Return the special Date '' (no date).""" |
635 | + return Date(NODATE) |
636 | + |
637 | + @classmethod |
638 | + def soon(cls): |
639 | + """Return the special Date 'soon'.""" |
640 | + return Date(SOON) |
641 | + |
642 | + @classmethod |
643 | + def later(cls): |
644 | + """Return the special Date 'tomorrow'.""" |
645 | + return Date(LATER) |
646 | + |
647 | + @classmethod |
648 | + def parse(cls, string): |
649 | + """Return a Date corresponding to *string*, or None. |
650 | + |
651 | + *string* may be in one of the following formats: |
652 | + * YYYY/MM/DD, YYYYMMDD, MMDD (assumes the current year), |
653 | + * any of the special values for Date, or |
654 | + * 'today', 'tomorrow', 'next week', 'next month' or 'next year' in |
655 | + English or the system locale. |
656 | + |
657 | + """ |
658 | + # sanitize input |
659 | + if string is None: |
660 | + string = '' |
661 | + else: |
662 | + sting = string.lower() |
663 | + # try the default formats |
664 | + try: |
665 | + return Date(string) |
666 | + except ValueError: |
667 | + pass |
668 | + today = datetime.date.today() |
669 | + # accepted date formats |
670 | + formats = { |
671 | + '%Y/%m/%d': 0, |
672 | + '%Y%m%d': 0, |
673 | + '%m%d': 0, |
674 | + _('today'): 0, |
675 | + 'tomorrow': 1, |
676 | + _('tomorrow'): 1, |
677 | + 'next week': 7, |
678 | + _('next week'): 7, |
679 | + 'next month': calendar.mdays[today.month], |
680 | + _('next month'): calendar.mdays[today.month], |
681 | + 'next year': 365 + int(calendar.isleap(today.year)), |
682 | + _('next year'): 365 + int(calendar.isleap(today.year)), |
683 | + } |
684 | + # add week day names in the current locale |
685 | + for i in range(7): |
686 | + formats[calendar.day_name[i]] = i + 7 - today.weekday() |
687 | + result = None |
688 | + # try all of the formats |
689 | + for fmt, offset in formats.iteritems(): |
690 | + try: # attempt to parse the string with known formats |
691 | + result = datetime.datetime.strptime(string, fmt) |
692 | + except ValueError: # parsing didn't work |
693 | + continue |
694 | + else: # parsing did work |
695 | + break |
696 | + if result: |
697 | + r = result.date() |
698 | + if r == datetime.date(1900, 1, 1): |
699 | + # a format like 'next week' was used that didn't get us a real |
700 | + # date value. Offset from today. |
701 | + result = today |
702 | + elif r.year == 1900: |
703 | + # a format like '%m%d' was used that got a real month and day, |
704 | + # but no year. Assume this year, or the next one if the day has |
705 | + # passed. |
706 | + if r.month >= today.month and r.day >= today.day: |
707 | + result = datetime.date(today.year, r.month, r.day) |
708 | + else: |
709 | + result = datetime.date(today.year + 1, r.month, r.day) |
710 | + return Date(result + datetime.timedelta(offset)) |
711 | + else: # can't parse this string |
712 | + raise ValueError("can't parse a valid date from %s" % string) |
713 | |
714 | def to_readable_string(self): |
715 | - if self.to_py_date() == NoDate().to_py_date(): |
716 | + if self._special == NODATE: |
717 | return None |
718 | - dleft = (self.to_py_date() - date.today()).days |
719 | + dleft = (self - datetime.date.today()).days |
720 | if dleft == 0: |
721 | - return _("Today") |
722 | + return _('Today') |
723 | elif dleft < 0: |
724 | abs_days = abs(dleft) |
725 | - return ngettext("Yesterday", "%(days)d days ago", abs_days) % \ |
726 | - {"days": abs_days} |
727 | + return ngettext('Yesterday', '%(days)d days ago', abs_days) % \ |
728 | + {'days': abs_days} |
729 | elif dleft > 0 and dleft <= 15: |
730 | - return ngettext("Tomorrow", "In %(days)d days", dleft) % \ |
731 | - {"days": dleft} |
732 | + return ngettext('Tomorrow', 'In %(days)d days', dleft) % \ |
733 | + {'days': dleft} |
734 | else: |
735 | - locale_format = self.__get_locale_string() |
736 | - if calendar.isleap(date.today().year): |
737 | + locale_format = locale.nl_langinfo(locale.D_FMT) |
738 | + if calendar.isleap(datetime.date.today().year): |
739 | year_len = 366 |
740 | else: |
741 | year_len = 365 |
742 | if float(dleft) / year_len < 1.0: |
743 | #if it's in less than a year, don't show the year field |
744 | locale_format = locale_format.replace('/%Y','') |
745 | - return self.to_py_date().strftime(locale_format) |
746 | - |
747 | - |
748 | -class FuzzyDate(Date): |
749 | - def __init__(self, offset, name): |
750 | - super(FuzzyDate, self).__init__() |
751 | - self.name=name |
752 | - self.offset=offset |
753 | - |
754 | - def to_py_date(self): |
755 | - return date.today()+timedelta(self.offset) |
756 | - |
757 | - def __str__(self): |
758 | - return _(self.name) |
759 | - |
760 | - def to_readable_string(self): |
761 | - return _(self.name) |
762 | - |
763 | - def xml_str(self): |
764 | - return self.name |
765 | - |
766 | - def days_left(self): |
767 | - return None |
768 | - |
769 | -class FuzzyDateFixed(FuzzyDate): |
770 | - def to_py_date(self): |
771 | - return self.offset |
772 | - |
773 | -NOW = FuzzyDate(0, _('now')) |
774 | -SOON = FuzzyDate(15, _('soon')) |
775 | -LATER = FuzzyDateFixed(date.max, _('later')) |
776 | - |
777 | -class RealDate(Date): |
778 | - def __init__(self, dt): |
779 | - super(RealDate, self).__init__() |
780 | - assert(dt is not None) |
781 | - self.proto = dt |
782 | - |
783 | - def to_py_date(self): |
784 | - return self.proto |
785 | - |
786 | - def __str__(self): |
787 | - return str(self.proto) |
788 | - |
789 | - def days_left(self): |
790 | - return (self.proto - date.today()).days |
791 | - |
792 | -DATE_MAX_MINUS_ONE = date.max-timedelta(1) # sooner than 'later' |
793 | -class NoDate(Date): |
794 | - |
795 | - def __init__(self): |
796 | - super(NoDate, self).__init__() |
797 | - |
798 | - def to_py_date(self): |
799 | - return DATE_MAX_MINUS_ONE |
800 | - |
801 | - def __str__(self): |
802 | - return '' |
803 | - |
804 | - def days_left(self): |
805 | - return None |
806 | - |
807 | - def __nonzero__(self): |
808 | - return False |
809 | -no_date = NoDate() |
810 | - |
811 | -#function to convert a string of the form YYYY-MM-DD |
812 | -#to a date |
813 | -#If the date is not correct, the function returns None |
814 | -def strtodate(stri) : |
815 | - if stri == _("now") or stri == "now": |
816 | - return NOW |
817 | - elif stri == _("soon") or stri == "soon": |
818 | - return SOON |
819 | - elif stri == _("later") or stri == "later": |
820 | - return LATER |
821 | - |
822 | - toreturn = None |
823 | - zedate = [] |
824 | - if stri : |
825 | - if '-' in stri : |
826 | - zedate = stri.split('-') |
827 | - elif '/' in stri : |
828 | - zedate = stri.split('/') |
829 | - |
830 | - if len(zedate) == 3 : |
831 | - y = zedate[0] |
832 | - m = zedate[1] |
833 | - d = zedate[2] |
834 | - if y.isdigit() and m.isdigit() and d.isdigit() : |
835 | - yy = int(y) |
836 | - mm = int(m) |
837 | - dd = int(d) |
838 | - # we catch exceptions here |
839 | - try : |
840 | - toreturn = date(yy,mm,dd) |
841 | - except ValueError: |
842 | - toreturn = None |
843 | - |
844 | - if not toreturn: return no_date |
845 | - else: return RealDate(toreturn) |
846 | - |
847 | - |
848 | -def date_today(): |
849 | - return RealDate(date.today()) |
850 | - |
851 | -def get_canonical_date(arg): |
852 | - """ |
853 | - Transform "arg" in a valid yyyy-mm-dd date or return None. |
854 | - "arg" can be a yyyy-mm-dd, yyyymmdd, mmdd, today, next week, |
855 | - next month, next year, or a weekday name. |
856 | - Literals are accepted both in english and in the locale language. |
857 | - When clashes occur the locale takes precedence. |
858 | - """ |
859 | - today = date.today() |
860 | - #FIXME: there surely exist a way to get day names from the datetime |
861 | - # or time module. |
862 | - day_names = ["monday", "tuesday", "wednesday", \ |
863 | - "thursday", "friday", "saturday", \ |
864 | - "sunday"] |
865 | - day_names_localized = [_("monday"), _("tuesday"), _("wednesday"), \ |
866 | - _("thursday"), _("friday"), _("saturday"), \ |
867 | - _("sunday")] |
868 | - delta_day_names = {"today": 0, \ |
869 | - "tomorrow": 1, \ |
870 | - "next week": 7, \ |
871 | - "next month": calendar.mdays[today.month], \ |
872 | - "next year": 365 + int(calendar.isleap(today.year))} |
873 | - delta_day_names_localized = \ |
874 | - {_("today"): 0, \ |
875 | - _("tomorrow"): 1, \ |
876 | - _("next week"): 7, \ |
877 | - _("next month"): calendar.mdays[today.month], \ |
878 | - _("next year"): 365 + int(calendar.isleap(today.year))} |
879 | - ### String sanitization |
880 | - arg = arg.lower() |
881 | - ### Conversion |
882 | - #yyyymmdd and mmdd |
883 | - if arg.isdigit(): |
884 | - if len(arg) == 4: |
885 | - arg = str(date.today().year) + arg |
886 | - assert(len(arg) == 8) |
887 | - arg = "%s-%s-%s" % (arg[:4], arg[4:6], arg[6:]) |
888 | - #today, tomorrow, next {week, months, year} |
889 | - elif arg in delta_day_names.keys() or \ |
890 | - arg in delta_day_names_localized.keys(): |
891 | - if arg in delta_day_names: |
892 | - delta = delta_day_names[arg] |
893 | - else: |
894 | - delta = delta_day_names_localized[arg] |
895 | - arg = (today + timedelta(days = delta)).isoformat() |
896 | - elif arg in day_names or arg in day_names_localized: |
897 | - if arg in day_names: |
898 | - arg_day = day_names.index(arg) |
899 | - else: |
900 | - arg_day = day_names_localized.index(arg) |
901 | - today_day = today.weekday() |
902 | - next_date = timedelta(days = arg_day - today_day + \ |
903 | - 7 * int(arg_day <= today_day)) + today |
904 | - arg = "%i-%i-%i" % (next_date.year, \ |
905 | - next_date.month, \ |
906 | - next_date.day) |
907 | - return strtodate(arg) |
908 | + return self._date.strftime(locale_format) |
909 | |
910 | |
911 | === modified file 'GTG/tools/taskxml.py' |
912 | --- GTG/tools/taskxml.py 2010-06-18 16:36:17 +0000 |
913 | +++ GTG/tools/taskxml.py 2010-06-20 04:28:25 +0000 |
914 | @@ -21,8 +21,8 @@ |
915 | import xml.dom.minidom |
916 | import xml.sax.saxutils as saxutils |
917 | |
918 | -from GTG.tools import cleanxml |
919 | -from GTG.tools import dates |
920 | +from GTG.tools import cleanxml |
921 | +from GTG.tools.dates import * |
922 | |
923 | #Take an empty task, an XML node and return a Task. |
924 | def task_from_xml(task,xmlnode) : |
925 | @@ -31,7 +31,7 @@ |
926 | uuid = "%s" %xmlnode.getAttribute("uuid") |
927 | cur_task.set_uuid(uuid) |
928 | donedate = cleanxml.readTextNode(xmlnode,"donedate") |
929 | - cur_task.set_status(cur_stat,donedate=dates.strtodate(donedate)) |
930 | + cur_task.set_status(cur_stat,donedate=Date.parse(donedate)) |
931 | #we will fill the task with its content |
932 | cur_task.set_title(cleanxml.readTextNode(xmlnode,"title")) |
933 | #the subtasks |
934 | @@ -54,9 +54,9 @@ |
935 | tas = "<content>%s</content>" %tasktext[0].firstChild.nodeValue |
936 | content = xml.dom.minidom.parseString(tas) |
937 | cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103 |
938 | - cur_task.set_due_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"duedate"))) |
939 | + cur_task.set_due_date(Date.parse(cleanxml.readTextNode(xmlnode,"duedate"))) |
940 | cur_task.set_modified(cleanxml.readTextNode(xmlnode,"modified")) |
941 | - cur_task.set_start_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"startdate"))) |
942 | + cur_task.set_start_date(Date.parse(cleanxml.readTextNode(xmlnode,"startdate"))) |
943 | cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",") |
944 | if "" in cur_tags: cur_tags.remove("") |
945 | for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag)) |
946 | @@ -74,10 +74,10 @@ |
947 | tags_str = tags_str + saxutils.escape(str(tag)) + "," |
948 | t_xml.setAttribute("tags", tags_str[:-1]) |
949 | cleanxml.addTextNode(doc,t_xml,"title",task.get_title()) |
950 | - cleanxml.addTextNode(doc,t_xml,"duedate", task.get_due_date().xml_str()) |
951 | + cleanxml.addTextNode(doc,t_xml,"duedate", str(task.get_due_date())) |
952 | cleanxml.addTextNode(doc,t_xml,"modified",task.get_modified_string()) |
953 | - cleanxml.addTextNode(doc,t_xml,"startdate", task.get_start_date().xml_str()) |
954 | - cleanxml.addTextNode(doc,t_xml,"donedate", task.get_closed_date().xml_str()) |
955 | + cleanxml.addTextNode(doc,t_xml,"startdate", str(task.get_start_date())) |
956 | + cleanxml.addTextNode(doc,t_xml,"donedate", str(task.get_closed_date())) |
957 | childs = task.get_children() |
958 | for c in childs : |
959 | cleanxml.addTextNode(doc,t_xml,"subtask",c) |
I hate to raise this after you did the work, but have you considered the possibility of using the pickle module? The serialization is done automatically like "pickle. loads(pickle. dumps(datetime. datetime. now())) "
I don't know how fast it is though.