Add calendar view

review395003
This commit is contained in:
Antoine Smolders
2013-06-09 20:13:45 +02:00
parent 16519559a1
commit df4b707855
12 changed files with 555 additions and 16 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add calendar view
* Add URL entry
* Remove request
+1
View File
@@ -1,3 +1,4 @@
Copyright (C) 2012-2013 Antoine Smolders.
Copyright (C) 2010-2013 Nicolas Évrard.
Copyright (C) 2007-2013 Cédric Krier.
Copyright (C) 2007-2013 Bertrand Chenal.
+1
View File
@@ -165,6 +165,7 @@ dist = setup(name=PACKAGE,
'timezone': ['pytz'],
'simplejson': ['simplejson'],
'cdecimal': ['cdecimal'],
'calendar': ['GooCalendar'],
},
**args
)
+1 -1
View File
@@ -367,7 +367,7 @@ class Record(SignalEvent):
self._check_load()
res = True
for field_name, field in self.group.fields.iteritems():
if fields and field_name not in fields:
if fields is not None and field_name not in fields:
continue
if field.get_state_attrs(self).get('readonly', False):
continue
+56 -5
View File
@@ -3,6 +3,8 @@
"Screen"
import copy
import gobject
import datetime
import calendar
try:
import simplejson as json
except ImportError:
@@ -185,6 +187,12 @@ class Screen(SignalEvent):
else:
domain = self.domain
if self.current_view.view_type == 'calendar':
if domain:
domain = ['AND', domain, self.current_view.current_domain()]
else:
domain = self.current_view.current_domain()
tab_domain = self.screen_container.get_tab_domain()
if tab_domain:
domain = ['AND', domain, tab_domain]
@@ -428,12 +436,15 @@ class Screen(SignalEvent):
return False
def new(self, default=True):
previous_view = self.current_view
if self.current_view.view_type == 'calendar':
selected_date = self.current_view.get_selected_date()
if (self.current_view
and ((self.current_view.view_type == 'tree'
and not (hasattr(self.current_view.widget_tree,
'editable')
and self.current_view.widget_tree.editable))
or self.current_view.view_type == 'graph')):
or self.current_view.view_type in ('graph', 'calendar'))):
self.switch_view('form')
if self.current_view.view_type != 'form':
return None
@@ -443,6 +454,8 @@ class Screen(SignalEvent):
group = self.group
record = group.new(default)
group.add(record, self.new_model_position())
if previous_view.view_type == 'calendar':
previous_view.set_default_date(record, selected_date)
self.current_record = record
self.display()
self.set_cursor(new=True)
@@ -674,10 +687,6 @@ class Screen(SignalEvent):
else:
self.current_record = None
if self.views:
#XXX To remove when calendar will be implemented
if self.current_view.view_type == 'calendar' and \
len(self.views) > 1:
self.switch_view()
self.search_active(self.current_view.view_type
in ('tree', 'graph', 'calendar'))
for view in self.views:
@@ -731,6 +740,27 @@ class Screen(SignalEvent):
record = next
break
self.current_record = record
elif view.view_type == 'calendar':
record = self.current_record
goocalendar = view.children['goocalendar']
date = goocalendar.selected_date
year = date.year
month = date.month
start = datetime.datetime(year, month, 1)
nb_days = calendar.monthrange(year, month)[1]
delta = datetime.timedelta(days=nb_days)
end = start + delta
events = goocalendar.event_store.get_events(start, end)
events.sort()
if not record:
self.current_record = len(events) and events[0].record
else:
for idx, event in enumerate(events):
if event.record == record:
next_id = idx + 1
if next_id < len(events):
self.current_record = events[next_id].record
break
else:
self.current_record = self.group[0] if len(self.group) else None
self.set_cursor(reset_view=False)
@@ -765,6 +795,27 @@ class Screen(SignalEvent):
if parent:
record = parent
self.current_record = record
elif view.view_type == 'calendar':
record = self.current_record
goocalendar = view.children['goocalendar']
date = goocalendar.selected_date
year = date.year
month = date.month
start = datetime.datetime(year, month, 1)
nb_days = calendar.monthrange(year, month)[1]
delta = datetime.timedelta(days=nb_days)
end = start + delta
events = goocalendar.event_store.get_events(start, end)
events.sort()
if not record:
self.current_record = len(events) and events[0].record
else:
for idx, event in enumerate(events):
if event.record == record:
prev_id = idx - 1
if prev_id >= 0:
self.current_record = events[prev_id].record
break
else:
self.current_record = self.group[-1] if len(self.group) else None
self.set_cursor(reset_view=False)
@@ -0,0 +1,73 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from functools import wraps
from interface import ParserView
def goocalendar_required(func):
"Decorator for goocalendar required"
@wraps(func)
def wrapper(self, *args, **kwargs):
if 'goocalendar' not in self.children:
return
return func(self, *args, **kwargs)
return wrapper
class ViewCalendar(ParserView):
def __init__(self, screen, widget, children=None, buttons=None,
notebooks=None, cursor_widget=None, children_field=None):
super(ViewCalendar, self).__init__(screen, widget, children, buttons,
notebooks, cursor_widget, children_field)
self.view_type = 'calendar'
def __getitem__(self, name):
return None
@goocalendar_required
def destroy(self):
self.widget.destroy()
self.children['goocalendar'].destroy()
@goocalendar_required
def get_selected_date(self):
return self.children['goocalendar'].selected_date
@goocalendar_required
def set_default_date(self, record, selected_date):
self.children['goocalendar'].set_default_date(record, selected_date)
def current_domain(self):
if 'goocalendar' in self.children:
return self.children['goocalendar'].current_domain()
else:
# No need to load any record as nothing will be shown
return [('id', '=', -1)]
def cancel(self):
pass
def set_value(self):
pass
def reset(self):
pass
@goocalendar_required
def display(self):
self.children['goocalendar'].display(self.screen.group)
gtkcal = self.children['toolbar'].gtkcal
if gtkcal and not gtkcal.is_drawable():
import goocanvas
# disable gtk.Calendar if it is not drawable anymore
self.children['toolbar'].gtkcal_item.set_property('visibility',
goocanvas.ITEM_INVISIBLE)
self.children['toolbar'].current_page.set_active(False)
def set_cursor(self, new=False, reset_view=True):
pass
def get_fields(self):
return []
@@ -0,0 +1,3 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
from parser import *
@@ -0,0 +1,120 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import calendar
import datetime
import goocalendar
import tryton.common as common
from dates_period import DatesPeriod
class Calendar_(goocalendar.Calendar):
'Calendar'
def __init__(self, attrs, model, root_node, fields, screen,
event_store=None):
super(Calendar_, self).__init__(event_store)
self.attrs = attrs
self.model = model
self.root_node = root_node
self.fields = fields
self.screen = screen
self.event_store = event_store
self.current_domain_period = self.get_displayed_period()
def destroy(self):
super(Calendar_, self).destroy()
def set_default_date(self, record, selected_date):
attrs = common.node_attributes(self.root_node)
dtstart = attrs.get('dtstart')
record[dtstart].set(record, datetime.datetime.combine(selected_date,
datetime.time(0)))
def get_displayed_period(self):
cal = calendar.Calendar(self.firstweekday)
if self.view == 'week':
week = goocalendar.util.my_weekdatescalendar(cal,
self.selected_date)
first_date = week[0]
last_date = week[6]
last_date += datetime.timedelta(1)
elif self.view == 'month':
weeks = goocalendar.util.my_monthdatescalendar(cal,
self.selected_date)
first_date = weeks[0][0]
last_date = weeks[5][6]
last_date += datetime.timedelta(1)
displayed_period = DatesPeriod(first_date, last_date)
return displayed_period
def update_domain(self):
displayed_period = self.get_displayed_period()
if not displayed_period.is_in(self.current_domain_period):
self.current_domain_period = displayed_period
return True
return False
def current_domain(self):
first_datetime, last_datetime = \
self.current_domain_period.get_dates(True)
attrs = common.node_attributes(self.root_node)
dtstart = attrs.get('dtstart')
dtend = attrs.get('dtend') or dtstart
domain = ['OR',
['AND', (dtstart, '>=', first_datetime),
(dtstart, '<', last_datetime)],
['AND', (dtend, '>=', first_datetime),
(dtend, '<', last_datetime)],
['AND', (dtstart, '<', first_datetime),
(dtend, '>', last_datetime)]]
return domain
def display(self, group):
attrs = common.node_attributes(self.root_node)
dtstart = attrs.get('dtstart')
dtend = attrs.get('dtend') or dtstart
if self.screen.current_record:
record = self.screen.current_record
date = record[dtstart].get(record)
if date: # select the day of the current record
self.select(date)
if self._event_store:
self._event_store.clear()
else:
event_store = goocalendar.EventStore()
self.event_store = event_store
fields = []
for node in self.root_node.childNodes:
if not node.nodeType == node.ELEMENT_NODE:
continue
if node.localName == 'field':
attrs = common.node_attributes(node)
fields.append(attrs.get('name'))
for record in group:
if not record[dtstart].get(record):
continue
start = record[dtstart].get(record)
end = record[dtend].get(record)
midnight = datetime.time(0)
all_day = False
if not isinstance(start, datetime.datetime):
start = datetime.datetime.combine(start, midnight)
if end and not isinstance(end, datetime.datetime):
end = datetime.datetime.combine(end, midnight)
all_day = True
elif not end:
all_day = True
# TODO define color code
label = '\n'.join(record[name].get_client(record)
for name in fields).rstrip()
event = goocalendar.Event(label, start, end,
bg_color='lightblue', all_day=all_day)
event.record = record
self._event_store.add(event)
self.grab_focus(self.get_root_item())
@@ -0,0 +1,31 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import datetime
class DatesPeriod():
"""
This class represents a period of time between two dates or two datetimes.
"""
def __init__(self, start, end):
assert type(start) == type(end)
self.start = start
self.end = end
def is_in(self, period):
return self.start >= period.start \
and self.end <= period.end
def get_dates(self, format_datetime=False):
if not format_datetime:
return self.start, self.end
midnight = datetime.time(0)
start = datetime.datetime.combine(self.start, midnight)
end = datetime.datetime.combine(self.end, midnight)
return start, end
def __str__(self):
string = self.start.__str__() + ' => ' + self.end.__str__()
return string
@@ -0,0 +1,85 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import datetime
import gettext
import gtk
from tryton.gui.window.view_form.view.interface import ParserInterface
import tryton.common as common
try:
from calendar_ import Calendar_
from toolbar import Toolbar
except ImportError:
Calendar_ = None
Toolbar = None
_ = gettext.gettext
class ParserCalendar(ParserInterface):
def on_page_changed(self, goocalendar, day, toolbar):
toolbar.update_displayed_date()
if goocalendar.update_domain():
self.screen.search_filter()
def on_view_changed(self, goocalendar, view, toolbar):
toolbar.update_displayed_date()
if goocalendar.update_domain():
self.screen.search_filter()
def on_event_pressed(self, goocalendar, event):
self.screen.current_record = event.record
def on_event_activated(self, goocalendar, event):
self.screen.switch_view('form')
def on_event_released(self, goocalendar, event, attrs):
dtstart = attrs.get('dtstart')
dtend = attrs.get('dtend') or dtstart
record = event.record
group = record.group
previous_start = record[dtstart].get(record)
new_start = event.start
new_end = event.end
if not isinstance(previous_start, datetime.datetime):
new_start = event.start.date()
new_end = event.end.date() if event.end else None
if previous_start <= new_start:
if new_end:
group.fields[dtend].set_client(record, new_end)
group.fields[dtstart].set_client(record, new_start)
else:
group.fields[dtstart].set_client(record, new_start)
if new_end:
group.fields[dtend].set_client(record, new_end)
goocalendar.select(new_start)
record.save()
def on_day_pressed(self, goocalendar, day):
self.screen.current_record = None
def on_day_activated(self, goocalendar, day):
self.screen.new()
def parse(self, model, root_node, fields):
attrs = common.node_attributes(root_node)
self.title = attrs.get('string', 'Unknown')
vbox = gtk.VBox()
if not Calendar_:
return vbox, {}, [], '', [], None
goocalendar = Calendar_(attrs=attrs, model=model, root_node=root_node,
fields=fields, screen=self.screen)
toolbar = Toolbar(goocalendar)
goocalendar.connect('view-changed', self.on_view_changed, toolbar)
goocalendar.connect('page-changed', self.on_page_changed, toolbar)
goocalendar.connect('event-pressed', self.on_event_pressed)
goocalendar.connect('event-activated', self.on_event_activated)
goocalendar.connect('event-released', self.on_event_released, attrs)
goocalendar.connect('day-pressed', self.on_day_pressed)
goocalendar.connect('day-activated', self.on_day_activated)
scrolledWindow = gtk.ScrolledWindow()
scrolledWindow.add_with_viewport(goocalendar)
vbox.pack_start(toolbar, False, False)
vbox.pack_start(scrolledWindow, True, True)
return vbox, {'root': scrolledWindow, 'toolbar': toolbar,
'goocalendar': goocalendar}, [], '', [], None
@@ -0,0 +1,176 @@
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import datetime
import calendar
import gettext
import gtk
import goocanvas
from tryton.gui import Main
_ = gettext.gettext
class Toolbar(gtk.Toolbar):
def __init__(self, goocalendar):
super(Toolbar, self).__init__()
self.goocalendar = goocalendar
self.accel_group = Main.get_main().accel_group
today_label = gtk.Label(_('Today'))
today_button = gtk.ToolButton(today_label)
today_button.set_homogeneous(False)
today_button.connect("clicked", self.on_today_button_clicked)
today_button.add_accelerator("clicked", self.accel_group,
gtk.keysyms.t, gtk.gdk.MODIFIER_MASK, gtk.ACCEL_VISIBLE)
self.insert(today_button, -1)
arrow_left = gtk.Arrow(gtk.ARROW_LEFT, gtk.SHADOW_NONE)
go_back = gtk.ToolButton(arrow_left, "go left")
go_back.set_expand(False)
go_back.set_homogeneous(False)
go_back.connect("clicked", self.on_go_back_clicked)
self.insert(go_back, -1)
self.current_page_label = gtk.Label("")
self.current_page = gtk.ToggleToolButton()
self.current_page.set_icon_widget(self.current_page_label)
self.current_page.connect("clicked", self.on_current_page_clicked)
self.insert(self.current_page, -1)
gtkcal = gtk.Calendar()
gtkcal.connect('day-selected', self.on_gtkcal_day_selected)
gtkcal.set_display_options(
gtk.CALENDAR_SHOW_HEADING |
gtk.CALENDAR_SHOW_WEEK_NUMBERS |
gtk.CALENDAR_SHOW_DAY_NAMES)
gtkcal.set_no_show_all(True)
gtkcal_item = goocanvas.Widget(widget=gtkcal)
gtkcal_item.set_property('visibility', goocanvas.ITEM_INVISIBLE)
goocalendar.get_root_item().add_child(gtkcal_item)
self.gtkcal = gtkcal
self.gtkcal_item = gtkcal_item
self.goocalendar.connect('day-selected',
self.on_goocalendar_day_selected)
arrow_right = gtk.Arrow(gtk.ARROW_RIGHT, gtk.SHADOW_NONE)
go_forward = gtk.ToolButton(arrow_right, "go right")
go_forward.set_expand(False)
go_forward.set_homogeneous(False)
go_forward.connect("clicked", self.on_go_forward_clicked)
self.insert(go_forward, -1)
arrow_left = gtk.Arrow(gtk.ARROW_LEFT, gtk.SHADOW_NONE)
previous_year = gtk.ToolButton(arrow_left, "next year")
previous_year.set_expand(False)
previous_year.set_homogeneous(False)
previous_year.connect("clicked", self.on_previous_year_clicked)
self.insert(previous_year, -1)
self.current_year_label = gtk.Label("")
current_year = gtk.ToolItem()
current_year.add(self.current_year_label)
self.insert(current_year, -1)
arrow_right = gtk.Arrow(gtk.ARROW_RIGHT, gtk.SHADOW_NONE)
next_year = gtk.ToolButton(arrow_right, "next year")
next_year.set_expand(False)
next_year.set_homogeneous(False)
next_year.connect("clicked", self.on_next_year_clicked)
self.insert(next_year, -1)
blank_widget = gtk.ToolItem()
blank_widget.set_expand(True)
self.insert(blank_widget, -1)
week_button = gtk.RadioToolButton()
week_button.set_label(_('Week View'))
week_button.connect("clicked", self.on_week_button_clicked)
week_button.add_accelerator("clicked", self.accel_group, gtk.keysyms.w,
gtk.gdk.MODIFIER_MASK, gtk.ACCEL_VISIBLE)
self.insert(week_button, -1)
month_view_label = gtk.Label(_('Month View'))
month_button = gtk.RadioToolButton(week_button)
month_button.set_icon_widget(month_view_label)
month_button.set_label(_('month_view'))
month_button.connect("clicked", self.on_month_button_clicked)
month_button.add_accelerator("clicked", self.accel_group,
gtk.keysyms.m, gtk.gdk.MODIFIER_MASK, gtk.ACCEL_VISIBLE)
month_button.set_active(True)
self.insert(month_button, -1)
self.update_displayed_date()
self.set_style(gtk.TOOLBAR_ICONS)
def update_displayed_date(self):
date = self.goocalendar.selected_date
year = date.timetuple()[0]
month = date.timetuple()[1]
day = date.timetuple()[2]
self.current_year_label.set_text(str(year))
if self.goocalendar.view == "month":
new_label = calendar.month_name[month]
self.current_page_label.set_text(new_label)
elif self.goocalendar.view == "week":
week_number = datetime.date(year, month, day).isocalendar()[1]
new_label = _('Week') + ' ' + str(week_number)
new_label += ' (' + calendar.month_name[month] + ')'
self.current_page_label.set_text(new_label)
def on_today_button_clicked(self, widget):
self.goocalendar.select(datetime.date.today())
def on_go_back_clicked(self, widget):
self.goocalendar.previous_page()
def on_current_page_clicked(self, widget):
gtkcal_item = self.gtkcal_item
if gtkcal_item.get_property('visibility') == goocanvas.ITEM_VISIBLE:
gtkcal_item.set_property('visibility', goocanvas.ITEM_INVISIBLE)
else:
gtkcal_item.set_property('visibility', goocanvas.ITEM_VISIBLE)
def on_gtkcal_day_selected(self, gtkcal):
year, month, day = gtkcal.get_date()
month += 1 # months go from 1 to 12 instead of from 0 to 11
self.goocalendar.select(datetime.date(year, month, day))
def on_goocalendar_day_selected(self, goocalendar, day):
# months go from 0 to 11 in gtk.Calendar instead of 1 to 12
new_date = self.goocalendar.selected_date
self.gtkcal.select_month(new_date.month - 1, new_date.year)
self.gtkcal.handler_block_by_func(self.on_gtkcal_day_selected)
self.gtkcal.select_day(new_date.day)
self.gtkcal.handler_unblock_by_func(self.on_gtkcal_day_selected)
def on_go_forward_clicked(self, widget):
self.goocalendar.next_page()
def on_previous_year_clicked(self, widget):
date = datetime.datetime.combine(self.goocalendar.selected_date,
datetime.time(0))
year, month, day = date.timetuple()[:3]
year -= 1
cal = calendar.Calendar(self.goocalendar.firstweekday)
next_month_days = [d for d in cal.itermonthdays(year, month)]
if day not in next_month_days:
day = max(next_month_days)
self.goocalendar.select(datetime.datetime(year, month, day))
def on_next_year_clicked(self, widget):
date = datetime.datetime.combine(self.goocalendar.selected_date,
datetime.time(0))
year, month, day = date.timetuple()[:3]
year += 1
cal = calendar.Calendar(self.goocalendar.firstweekday)
next_month_days = [d for d in cal.itermonthdays(year, month)]
if day not in next_month_days:
day = max(next_month_days)
self.goocalendar.select(datetime.datetime(year, month, day))
def on_week_button_clicked(self, widget):
self.goocalendar.set_view("week")
def on_month_button_clicked(self, widget):
self.goocalendar.set_view("month")
@@ -4,32 +4,31 @@ from interface import ParserInterface
import form_gtk
import list_gtk
import graph_gtk
#import calendar_gtk
import calendar_gtk
from form import ViewForm
from list import ViewList
from graph import ViewGraph
#from calendar import ViewCalendar
from calendar_ import ViewCalendar
from tryton.exceptions import TrytonError
PARSERS = {
'form': form_gtk.ParserForm,
'tree': list_gtk.ParserTree,
'graph': graph_gtk.ParserGraph,
#'calendar': calendar_gtk.parser_calendar,
}
'calendar': calendar_gtk.ParserCalendar,
}
PARSERS2 = {
'form': ViewForm,
'tree': ViewList,
'graph': ViewGraph,
#'calendar': ViewCalendar,
}
'calendar': ViewCalendar,
}
class WidgetParse(ParserInterface):
def parse(self, screen, root_node, fields, children_field=None):
widget = None
for node in root_node.childNodes:
if not node.nodeType == node.ELEMENT_NODE:
continue
@@ -43,8 +42,6 @@ class WidgetParse(ParserInterface):
res = PARSERS2[node.localName](screen, wid, child,
state_widgets, notebooks, cursor_widget, children_field)
res.title = widget.title
widget = res
break
return res
else:
raise TrytonError('Unknow view mode: %s' % node.localName)
return widget