Merge lp:~gtg-contributors/gtg/new-date-class into lp:~gtg/gtg/old-trunk

Proposed by Paul Natsuo Kishimoto on 2010-06-20
Status: Merged
Approved by: Izidor Matušov on 2012-02-14
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
Reviewer Review Type Date Requested Status
Paul Natsuo Kishimoto (community) Approve on 2010-12-17
Bryce Harrington (community) code 2010-06-20 Approve on 2010-07-14
Review via email: mp+28009@code.launchpad.net

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.date.today() # get a date instance representing today
  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.

To post a comment you must log in.
Luca Invernizzi (invernizzi) wrote :

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.

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_canonical_date(string))

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_date(...) so that string == str(Date.parse(string)).

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.

review: Approve (code)
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?

review: Resubmit
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches

to status/vote changes: