GTG

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

Proposed by Paul Natsuo Kishimoto
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
Reviewer Review Type Date Requested Status
Paul Natsuo Kishimoto (community) Approve
Bryce Harrington (community) code Approve
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.
Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
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)
Revision history for this message
Paul Natsuo Kishimoto (khaeru) wrote :

Anybody in gtg-devs feel like committing this? :)

Revision history for this message
Paul Natsuo Kishimoto (khaeru) wrote :

Once again, can this be either committed or rejected?

review: Needs Resubmitting
Revision history for this message
Paul Natsuo Kishimoto (khaeru) :
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: