diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py
index 1ff7777cf36..d602fce3a0f 100644
--- a/addons/base_calendar/base_calendar.py
+++ b/addons/base_calendar/base_calendar.py
@@ -306,7 +306,7 @@ class calendar_attendee(osv.osv):
res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
return [(r['object'], r['name']) for r in res]
- def _lang_get(self, cr, uid, context={}):
+ def _lang_get(self, cr, uid, context=None):
"""
Get language for language selection field.
@param cr: the current row, from the database cursor,
@@ -912,10 +912,10 @@ class calendar_event(osv.osv):
_description = "Calendar Event"
__attribute__ = {}
- def _tz_get(self, cr, uid, context={}):
+ def _tz_get(self, cr, uid, context=None):
return [(x.lower(), x) for x in pytz.all_timezones]
- def onchange_allday(self, cr, uid, ids, allday, context={}):
+ def onchange_allday(self, cr, uid, ids, allday, context=None):
"""Sets duration as 24 Hours if event is selcted for all day
@param self: The object pointer
@param cr: the current row, from the database cursor,
@@ -1017,7 +1017,8 @@ class calendar_event(osv.osv):
elif int(val.get('interval')) > 1: #If interval is other than 1 rule is custom
rrule_type = 'custom'
- qry = "UPDATE %(table)s set rrule_type=\'%(rule_type)s\' "
+ qry = "UPDATE \"%s\" set rrule_type=%%s " % self._table
+ qry_args = [ rrule_type, ]
if rrule_type == 'custom':
new_val = val.copy()
@@ -1055,17 +1056,12 @@ class calendar_event(osv.osv):
new_val.pop('bymonth')
for k, v in new_val.items():
- temp = ", %s='%s'" % (k, v)
- qry += temp
+ qry += ", %s=%%s" % k
+ qry_args.append(v)
- whr = " where id=%(id)s"
- qry = qry + whr
- val.update({
- 'table': self._table,
- 'rule_type': rrule_type,
- 'id': id,
- })
- cr.execute(qry % val)
+ qry = qry + " where id=%s"
+ qry_args.append(id)
+ cr.execute(qry, qry_args)
return True
def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
@@ -1479,6 +1475,8 @@ true, it will allow you to hide the event alarm information without removing it.
if not real_event_id in new_ids:
new_ids.append(real_event_id)
+ if vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
+ vals['vtimezone'] = vals['vtimezone'][40:]
if new_ids:
res = super(calendar_event, self).write(cr, uid, new_ids, vals, context=context)
if (vals.has_key('alarm_id') or vals.has_key('base_calendar_alarm_id'))\
@@ -1489,7 +1487,7 @@ true, it will allow you to hide the event alarm information without removing it.
context=context)
return res
- def browse(self, cr, uid, ids, context=None, list_class=None, fields_process={}):
+ def browse(self, cr, uid, ids, context=None, list_class=None, fields_process=None):
"""
Overrides orm browse method.
@param self: the object pointer
@@ -1608,6 +1606,8 @@ true, it will allow you to hide the event alarm information without removing it.
"""
if not context:
context = {}
+ if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
+ vals['vtimezone'] = vals['vtimezone'][40:]
res = super(calendar_event, self).create(cr, uid, vals, context)
alarm_obj = self.pool.get('res.alarm')
alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
@@ -1682,7 +1682,7 @@ class calendar_todo(osv.osv):
@param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
@param context: A standard dictionary for contextual values
"""
-
+
assert name == 'date'
return self.write(cr, uid, id, { 'date_start': value }, context=context)
diff --git a/addons/base_calendar/security/ir.model.access.csv b/addons/base_calendar/security/ir.model.access.csv
index 0ae628fb720..aa269e9ff18 100644
--- a/addons/base_calendar/security/ir.model.access.csv
+++ b/addons/base_calendar/security/ir.model.access.csv
@@ -1,5 +1,5 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
-"access_calendar_attendee","calendar.attendee","model_calendar_attendee","base.group_user",1,1,1,0
+"access_calendar_attendee","calendar.attendee","model_calendar_attendee","base.group_user",1,1,1,1
"access_calendar_alarm","calendar.alarm","model_calendar_alarm","base.group_user",1,1,1,1
"access_res_alarm","res.alarm","model_res_alarm","base.group_user",1,1,1,1
"access_calendar_todo","calendar.todo","model_calendar_todo","base.group_user",1,1,1,1
diff --git a/addons/caldav/__openerp__.py b/addons/caldav/__openerp__.py
index cdb06a17a1f..40088cb0bac 100644
--- a/addons/caldav/__openerp__.py
+++ b/addons/caldav/__openerp__.py
@@ -21,12 +21,12 @@
{
- "name" : "Share Calendar using Caldav",
- "version" : "1.0",
+ "name" : "Share Calendar using CalDAV",
+ "version" : "1.1",
"depends" : [
"base",
"document_webdav",
- ],
+ ],
'description': """
This module Contains basic functionality for caldav system like:
- Webdav server that provides remote access to calendar
@@ -34,7 +34,10 @@
- Customize calendar event and todo attribute with any of OpenERP model
- Provides iCal Import/Export functionality
- To access OpenERP Calendar using caldav to remote site use the URL like:
+ To access Calendars using CalDAV clients, point them to:
+ http://HOSTNAME:PORT/webdav/DATABASE_NAME/calendars/users/USERNAME/c
+
+ To access OpenERP Calendar using WebCal to remote site use the URL like:
http://HOSTNAME:PORT/webdav/DATABASE_NAME/Calendars/CALENDAR_NAME.ics
Where,
@@ -53,7 +56,8 @@
'wizard/calendar_event_export_view.xml',
'wizard/calendar_event_import_view.xml',
'wizard/calendar_event_subscribe_view.xml',
- 'caldav_view.xml'
+ 'caldav_view.xml',
+ 'caldav_setup.xml'
],
"installable" : True,
"active" : False,
diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py
index 11f720bdcab..b735d853025 100644
--- a/addons/caldav/caldav_node.py
+++ b/addons/caldav/caldav_node.py
@@ -20,41 +20,29 @@
##############################################################################
import time
-from document import nodes
+from document_webdav import nodes
+from document.nodes import _str2time
+import logging
import StringIO
+from orm_utils import get_last_modified
-class node_database(nodes.node_database):
- def _child_get(self, cr, name=False, parent_id=False, domain=None):
- dirobj = self.context._dirobj
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- if not domain:
- domain = []
- domain2 = domain + [('calendar_collection','=', False)]
- res = super(node_database, self)._child_get(cr, name=name, parent_id=parent_id, domain=domain2)
- where = [('parent_id','=',parent_id)]
- domain2 = domain + [('calendar_collection','=', True)]
- if name:
- where.append(('name','=',name))
- if domain2:
- where += domain2
+try:
+ from tools.dict_tools import dict_merge, dict_merge2
+except ImportError:
+ from document.dict_tools import dict_merge, dict_merge2
- where2 = where + [('type', '=', 'directory')]
- ids = dirobj.search(cr, uid, where2, context=ctx)
- for dirr in dirobj.browse(cr,uid,ids,context=ctx):
- res.append(node_calendar_collection(dirr.name,self,self.context,dirr))
- return res
+# TODO: implement DAV-aware errors, inherit from IOError
+
+# Assuming that we have set global properties right, we mark *all*
+# directories as having calendar-access.
+nodes.node_dir.http_options = dict_merge2(nodes.node_dir.http_options,
+ { 'DAV': ['calendar-access',] })
class node_calendar_collection(nodes.node_dir):
- DAV_PROPS = {
- "http://calendarserver.org/ns/" : ('getctag',),
- }
- DAV_M_NS = {
- "http://calendarserver.org/ns/" : '_get_dav',
- }
-
- http_options = { 'DAV': ['calendar-access'] }
+ DAV_PROPS = dict_merge2(nodes.node_dir.DAV_PROPS,
+ { "http://calendarserver.org/ns/" : ('getctag',), } )
+ DAV_M_NS = dict_merge2(nodes.node_dir.DAV_M_NS,
+ { "http://calendarserver.org/ns/" : '_get_dav', } )
def _file_get(self,cr, nodename=False):
return []
@@ -80,45 +68,117 @@ class node_calendar_collection(nodes.node_dir):
for cal in fil_obj.browse(cr, uid, ids, context=ctx):
if (not name) or not ext:
res.append(node_calendar(cal.name, self, self.context, cal))
- if (not name) or ext:
+ if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
+ # these ones must not see the webcal entry.
+ continue
+ if cal.has_webcal and (not name) or ext:
res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal))
# May be both of them!
return res
- def _get_dav_owner(self, cr):
- # Todo?
- return False
-
def _get_ttag(self, cr):
return 'calen-dir-%d' % self.dir_id
def _get_dav_getctag(self, cr):
- result = self.get_etag(cr)
- return str(result)
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('collection_id','=',self.dir_id)]
+ bc_obj = dirobj.pool.get('basic.calendar')
+
+ res = get_last_modified(bc_obj, cr, uid, where, context=ctx)
+ return _str2time(res)
+
+class node_calendar_res_col(nodes.node_res_obj):
+ """ Calendar collection, as a dynamically created node
+
+ This class shall be used instead of node_calendar_collection, when the
+ node is under dynamic ones.
+ """
+ DAV_PROPS = dict_merge2(nodes.node_res_obj.DAV_PROPS,
+ { "http://calendarserver.org/ns/" : ('getctag',), } )
+ DAV_M_NS = dict_merge2(nodes.node_res_obj.DAV_M_NS,
+ { "http://calendarserver.org/ns/" : '_get_dav', } )
+
+ def _file_get(self,cr, nodename=False):
+ return []
+
+ def _child_get(self, cr, name=False, parent_id=False, domain=None):
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('collection_id','=',self.dir_id)]
+ ext = False
+ if name and name.endswith('.ics'):
+ name = name[:-4]
+ ext = True
+ if name:
+ where.append(('name','=',name))
+ if not domain:
+ domain = []
+ where = where + domain
+ fil_obj = dirobj.pool.get('basic.calendar')
+ ids = fil_obj.search(cr,uid,where,context=ctx)
+ res = []
+ # TODO: shall we use any of our dynamic information??
+ for cal in fil_obj.browse(cr, uid, ids, context=ctx):
+ if (not name) or not ext:
+ res.append(node_calendar(cal.name, self, self.context, cal))
+ if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
+ # these ones must not see the webcal entry.
+ continue
+ if cal.has_webcal and (not name) or ext:
+ res.append(res_node_calendar(cal.name+'.ics', self, self.context, cal))
+ # May be both of them!
+ return res
+
+ def _get_ttag(self, cr):
+ return 'calen-dir-%d' % self.dir_id
+
+ def _get_dav_getctag(self, cr):
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('collection_id','=',self.dir_id)]
+ bc_obj = dirobj.pool.get('basic.calendar')
+
+ res = get_last_modified(bc_obj, cr, uid, where, context=ctx)
+ return _str2time(res)
class node_calendar(nodes.node_class):
our_type = 'collection'
DAV_PROPS = {
- "DAV:": ('principal-collection-set'),
- "http://cal.me.com/_namespace/" : ('user-state'),
- "http://calendarserver.org/ns/" : (
- 'dropbox-home-URL',
- 'notification-URL',
- 'getctag',),
+ "DAV:": ('supported-report-set',),
+ # "http://cal.me.com/_namespace/" : ('user-state',),
+ "http://calendarserver.org/ns/" : ( 'getctag',),
'http://groupdav.org/': ('resourcetype',),
"urn:ietf:params:xml:ns:caldav" : (
- 'calendar-description',
+ 'calendar-description',
+ 'supported-calendar-component-set',
+ ),
+ "http://apple.com/ns/ical/": ("calendar-color", "calendar-order"),
+ }
+ DAV_PROPS_HIDDEN = {
+ "urn:ietf:params:xml:ns:caldav" : (
'calendar-data',
- 'calendar-home-set',
- 'calendar-user-address-set',
- 'schedule-inbox-URL',
- 'schedule-outbox-URL',)}
+ 'calendar-timezone',
+ 'supported-calendar-data',
+ 'max-resource-size',
+ 'min-date-time',
+ 'max-date-time',
+ )}
+
DAV_M_NS = {
"DAV:" : '_get_dav',
- "http://cal.me.com/_namespace/": '_get_dav',
+ # "http://cal.me.com/_namespace/": '_get_dav',
'http://groupdav.org/': '_get_gdav',
"http://calendarserver.org/ns/" : '_get_dav',
- "urn:ietf:params:xml:ns:caldav" : '_get_caldav'}
+ "urn:ietf:params:xml:ns:caldav" : '_get_caldav',
+ "http://apple.com/ns/ical/": '_get_apple_cal',
+ }
http_options = { 'DAV': ['calendar-access'] }
@@ -131,42 +191,33 @@ class node_calendar(nodes.node_class):
self.content_length = 0
self.displayname = calendar.name
self.cal_type = calendar.type
+ self.cal_color = calendar.calendar_color or None
+ self.cal_order = calendar.calendar_order or None
+ try:
+ self.uuser = (calendar.user_id and calendar.user_id.login) or 'nobody'
+ except Exception:
+ self.uuser = 'nobody'
def _get_dav_getctag(self, cr):
- result = self._get_ttag(cr) + ':' + str(time.time())
- return str(result)
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
- def _get_dav_dropbox_home_URL(self, cr):
- import urllib
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- calendar_obj = self.context._dirobj.pool.get('basic.calendar')
- calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
- res = '%s/%s' %(calendar.collection_id.name, calendar.name)
- url = urllib.quote('/%s/%s' % (cr.dbname, res))
- return url
-
- def _get_dav_notification_URL(self, cr):
- import urllib
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- calendar_obj = self.context._dirobj.pool.get('basic.calendar')
- calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
- res = '%s/%s' %(calendar.collection_id.name, calendar.name)
- url = urllib.quote('/%s/%s' % (cr.dbname, res))
- return url
+ bc_obj = dirobj.pool.get('basic.calendar')
+ res = bc_obj.get_cal_max_modified(cr, uid, [self.calendar_id], self, domain=[], context=ctx)
+ return _str2time(res)
def _get_dav_user_state(self, cr):
#TODO
- return True
-
+ return 'online'
def get_dav_resourcetype(self, cr):
res = [ ('collection', 'DAV:'),
- (str(self.cal_type + '-collection'), 'http://groupdav.org/'),
- ('calendar', 'urn:ietf:params:xml:ns:caldav') ]
+ ('calendar', 'urn:ietf:params:xml:ns:caldav'),
+ ]
+ if self.context.get('DAV-client', '') == 'GroupDAV':
+ res.append((str(self.cal_type + '-collection'), 'http://groupdav.org/'))
return res
def get_domain(self, cr, filters):
@@ -174,6 +225,7 @@ class node_calendar(nodes.node_class):
res = []
if not filters:
return res
+ _log = logging.getLogger('caldav.query')
if filters.localName == 'calendar-query':
res = []
for filter_child in filters.childNodes:
@@ -189,26 +241,33 @@ class node_calendar(nodes.node_class):
if vevent_filter.nodeType == vevent_filter.TEXT_NODE:
continue
if vevent_filter.localName == 'comp-filter':
- if vevent_filter.getAttribute('name') == 'VEVENT':
- res = [('type','=','vevent')]
- if vevent_filter.getAttribute('name') == 'VTODO':
- res = [('type','=','vtodo')]
+ if vevent_filter.getAttribute('name'):
+ res = [('type','=',vevent_filter.getAttribute('name').lower() )]
+
+ for cfe in vevent_filter.childNodes:
+ if cfe.localName == 'time-range':
+ if cfe.getAttribute('start'):
+ _log.warning("Ignore start.. ")
+ # No, it won't work in this API
+ #val = cfe.getAttribute('start')
+ #res += [('dtstart','=', cfe)]
+ elif cfe.getAttribute('end'):
+ _log.warning("Ignore end.. ")
+ else:
+ _log.debug("Unknown comp-filter: %s", cfe.localName)
+ else:
+ _log.debug("Unknown comp-filter: %s", vevent_filter.localName)
+ else:
+ _log.debug("Unknown filter element: %s", vcalendar_filter.localName)
+ else:
+ _log.debug("Unknown calendar-query element: %s", filter_child.localName)
return res
elif filters.localName == 'calendar-multiget':
- names = []
- for filter_child in filters.childNodes:
- if filter_child.nodeType == filter_child.TEXT_NODE:
- continue
- if filter_child.localName == 'href':
- if not filter_child.firstChild:
- continue
- uri = filter_child.firstChild.data
- caluri = uri.split('/')
- if len(caluri):
- caluri = caluri[-2]
- if caluri not in names : names.append(caluri)
- res = [('name','in',names)]
- return res
+ # this is not the place to process, as it wouldn't support multi-level
+ # hrefs. So, the code is moved to document_webdav/dav_fs.py
+ pass
+ else:
+ _log.debug("Unknown element in REPORT: %s", filters.localName)
return res
def children(self, cr, domain=None):
@@ -227,26 +286,33 @@ class node_calendar(nodes.node_class):
ctx = self.context.context.copy()
ctx.update(self.dctx)
where = []
+ bc_obj = dirobj.pool.get('basic.calendar')
+
if name:
if name.endswith('.ics'):
name = name[:-4]
try:
- where.append(('id','=',int(name)))
+ if name.isdigit():
+ where.append(('id','=',int(name)))
+ else:
+ bca_obj = dirobj.pool.get('basic.calendar.alias')
+ bc_alias = bca_obj.search(cr, uid,
+ [('cal_line_id.calendar_id', '=', self.calendar_id),
+ ('name', '=', name)] )
+ if not bc_alias:
+ return []
+ bc_val = bca_obj.read(cr, uid, bc_alias, ['res_id',])
+ where.append(('id', '=', bc_val[0]['res_id']))
except ValueError:
# if somebody requests any other name than the ones we
# generate (non-numeric), it just won't exist
- # FIXME: however, this confuses Evolution (at least), which
- # thinks the .ics node hadn't been saved.
return []
if not domain:
domain = []
- fil_obj = dirobj.pool.get('basic.calendar')
- ids = fil_obj.search(cr, uid, domain)
- res = []
- if self.calendar_id in ids:
- res = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=where, context=ctx)
+ # we /could/ be supplying an invalid calendar id to bc_obj, it has to check
+ res = bc_obj.get_calendar_objects(cr, uid, [self.calendar_id], self, domain=where, context=ctx)
return res
def create_child(self, cr, path, data):
@@ -268,7 +334,24 @@ class node_calendar(nodes.node_class):
assert isinstance(res[0], (int, long))
fnodes = fil_obj.get_calendar_objects(cr, uid, [self.calendar_id], self,
domain=[('id','=',res[0])], context=ctx)
+ if self.context.get('DAV-client','') in ('iPhone', 'iCalendar',):
+ # For those buggy clients, register the alias
+ bca_obj = fil_obj.pool.get('basic.calendar.alias')
+ ourcal = fil_obj.browse(cr, uid, self.calendar_id)
+ line_id = None
+ for line in ourcal.line_ids:
+ if line.name == ourcal.type:
+ line_id = line.id
+ break
+ assert line_id, "Calendar #%d must have at least one %s line" % \
+ (ourcal.id, ourcal.type)
+ if path.endswith('.ics'):
+ path = path[:-4]
+ bca_obj.create(cr, uid, { 'cal_line_id': line_id,
+ 'res_id': res[0], 'name': path}, context=ctx)
return fnodes[0]
+ # If we reach this line, it means that we couldn't import any useful
+ # (and matching type vs. our node kind) data from the iCal content.
return None
@@ -287,8 +370,12 @@ class node_calendar(nodes.node_class):
def rmcol(self, cr):
return False
-
def _get_caldav_calendar_data(self, cr):
+ if self.context.get('DAV-client', '') in ('iPhone', 'iCalendar'):
+ # Never return collective data to iClients, they get confused
+ # because they do propfind on the calendar node with Depth=1
+ # and only expect the childrens' data
+ return None
res = []
for child in self.children(cr):
res.append(child._get_caldav_calendar_data(cr))
@@ -299,86 +386,54 @@ class node_calendar(nodes.node_class):
calendar_obj = self.context._dirobj.pool.get('basic.calendar')
ctx = self.context.context.copy()
ctx.update(self.dctx)
- calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
- return calendar.description
+ try:
+ calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
+ return calendar.description or calendar.name
+ except Exception, e:
+ return None
- def _get_dav_principal_collection_set(self, cr):
- import xml
- import urllib
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- calendar_obj = self.context._dirobj.pool.get('basic.calendar')
- calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
- res = '%s/%s' %(calendar.collection_id.name, calendar.name)
- doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None)
- href = doc.documentElement
- href.tagName = 'D:href'
- huri = doc.createTextNode(urllib.quote('/%s/%s' % (cr.dbname, res)))
- href.appendChild(huri)
- return href
+ def _get_dav_supported_report_set(self, cr):
+
+ return ('supported-report', 'DAV:',
+ ('report','DAV:',
+ ('principal-match','DAV:')
+ )
+ )
- def _get_caldav_calendar_home_set(self, cr):
- import xml.dom.minidom
- import urllib
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None)
+ def _get_caldav_supported_calendar_component_set(self, cr):
+ return ('comp', 'urn:ietf:params:xml:ns:caldav', None,
+ {'name': self.cal_type.upper()} )
+
+ def _get_caldav_calendar_timezone(self, cr):
+ return None #TODO
+
+ def _get_caldav_supported_calendar_data(self, cr):
+ return ('calendar-data', 'urn:ietf:params:xml:ns:caldav', None,
+ {'content-type': "text/calendar", 'version': "2.0" } )
+
+ def _get_caldav_max_resource_size(self, cr):
+ return 65535
- calendar_obj = self.context._dirobj.pool.get('basic.calendar')
- calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
- huri = doc.createTextNode(urllib.quote('/%s/%s' % (cr.dbname, calendar.collection_id.name)))
- href = doc.documentElement
- href.tagName = 'D:href'
- href.appendChild(huri)
- return href
+ def _get_caldav_min_date_time(self, cr):
+ return "19700101T000000Z"
- def _get_caldav_calendar_user_address_set(self, cr):
- import xml.dom.minidom
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- user_obj = self.context._dirobj.pool.get('res.users')
- user = user_obj.browse(cr, uid, uid, context=ctx)
- doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None)
- href = doc.documentElement
- href.tagName = 'D:href'
- huri = doc.createTextNode('MAILTO:' + str(user.email) or str(user.name))
- href.appendChild(huri)
- return href
-
- def _get_caldav_schedule_outbox_URL(self, cr):
- return self._get_caldav_schedule_inbox_URL(cr)
-
- def _get_caldav_schedule_inbox_URL(self, cr):
- import xml.dom.minidom
- import urllib
- uid = self.context.uid
- ctx = self.context.context.copy()
- ctx.update(self.dctx)
- calendar_obj = self.context._dirobj.pool.get('basic.calendar')
- calendar = calendar_obj.browse(cr, uid, self.calendar_id, context=ctx)
- res = '%s/%s' %(calendar.collection_id.name, calendar.name)
- doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'href', None)
- href = doc.documentElement
- href.tagName = 'D:href'
- huri = doc.createTextNode(urllib.quote('/%s/%s' % (cr.dbname, res)))
- href.appendChild(huri)
- return href
+ def _get_caldav_max_date_time(self, cr):
+ return "21001231T235959Z" # I will be dead by then
+
+ def _get_apple_cal_calendar_color(self, cr):
+ return self.cal_color
+ def _get_apple_cal_calendar_order(self, cr):
+ return self.cal_order
class res_node_calendar(nodes.node_class):
our_type = 'file'
DAV_PROPS = {
- "http://calendarserver.org/ns/" : ('getctag'),
+ "http://calendarserver.org/ns/" : ('getctag',),
"urn:ietf:params:xml:ns:caldav" : (
'calendar-description',
'calendar-data',
- 'calendar-home-set',
- 'calendar-user-address-set',
- 'schedule-inbox-URL',
- 'schedule-outbox-URL',)}
+ )}
DAV_M_NS = {
"http://calendarserver.org/ns/" : '_get_dav',
"urn:ietf:params:xml:ns:caldav" : '_get_caldav'}
@@ -393,7 +448,7 @@ class res_node_calendar(nodes.node_class):
self.calendar_id = hasattr(parent, 'calendar_id') and parent.calendar_id or False
if res_obj:
if not self.calendar_id: self.calendar_id = res_obj.id
- pr = res_obj.perm_read()[0]
+ pr = res_obj.perm_read(context=context, details=False)[0]
self.create_date = pr.get('create_date')
self.write_date = pr.get('write_date') or pr.get('create_date')
self.displayname = res_obj.name
@@ -414,6 +469,7 @@ class res_node_calendar(nodes.node_class):
uid = self.context.uid
calendar_obj = self.context._dirobj.pool.get('basic.calendar')
context = self.context.context.copy()
+ context.update(self.dctx)
context.update({'model': self.model, 'res_id':self.res_id})
res = calendar_obj.export_cal(cr, uid, [self.calendar_id], context=context)
return res
@@ -426,8 +482,11 @@ class res_node_calendar(nodes.node_class):
def set_data(self, cr, data, fil_obj = None):
uid = self.context.uid
+ context = self.context.context.copy()
+ context.update(self.dctx)
+ context.update({'model': self.model, 'res_id':self.res_id})
calendar_obj = self.context._dirobj.pool.get('basic.calendar')
- res = calendar_obj.import_cal(cr, uid, data, self.calendar_id)
+ res = calendar_obj.import_cal(cr, uid, data, self.calendar_id, context=context)
return res
def _get_ttag(self,cr):
@@ -438,7 +497,6 @@ class res_node_calendar(nodes.node_class):
res = '%d' % (self.calendar_id)
return res
-
def rm(self, cr):
uid = self.context.uid
res = False
diff --git a/addons/caldav/caldav_setup.xml b/addons/caldav/caldav_setup.xml
new file mode 100644
index 00000000000..c33edbc75f7
--- /dev/null
+++ b/addons/caldav/caldav_setup.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+ []
+
+
+
+
+
+ directory
+
+ calendars
+
+
+ []
+
+
+
+
+ directory
+
+ resources
+
+
+ []
+
+
+
+
+
+ ressource
+
+
+ __uids__
+
+
+ []
+
+
+
+
+
+
+ ressource
+
+
+ users
+
+
+
+ user_id
+ res_id
+
+
+ []
+
+
+
+
+
+ directory
+
+ c
+
+
+
+ []
+
+
+
+
+
+ directory
+
+ groups
+
+
+
+ urn:ietf:params:xml:ns:caldav
+
+ calendar-home-set
+ ('href','DAV:','/%s/%s/calendars/users/%s/c' % ('webdav',dbname,username) )
+
+
+
+
diff --git a/addons/caldav/caldav_view.xml b/addons/caldav/caldav_view.xml
index 2e9719efead..97656a954ad 100644
--- a/addons/caldav/caldav_view.xml
+++ b/addons/caldav/caldav_view.xml
@@ -74,6 +74,9 @@
+
+
+
diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py
index 068af56459f..ccc4bf39fac 100644
--- a/addons/caldav/calendar.py
+++ b/addons/caldav/calendar.py
@@ -30,7 +30,10 @@ import pytz
import re
import tools
import time
+import logging
from caldav_node import res_node_calendar
+from orm_utils import get_last_modified
+from tools.safe_eval import safe_eval as eval
try:
import vobject
@@ -63,12 +66,13 @@ def uid2openobjectid(cr, uidval, oomodel, rdate):
return (False, None)
qry = 'SELECT DISTINCT(id) FROM %s' % model_obj._table
if rdate:
- qry += " where recurrent_id='%s'" % (rdate) #TOFIX: sql injection
- cr.execute(qry)
+ qry += " WHERE recurrent_id=%s"
+ cr.execute(qry, (rdate,))
r_id = cr.fetchone()
if r_id:
return (id, r_id[0])
-
+ else:
+ return (False, None)
cr.execute(qry)
ids = map(lambda x: str(x[0]), cr.fetchall())
if id in ids:
@@ -232,6 +236,7 @@ def map_data(cr, uid, obj, context=None):
class CalDAV(object):
__attribute__ = {}
+ _logger = logging.getLogger('document.caldav')
def ical_set(self, name, value, type):
""" set calendar Attribute
@@ -294,6 +299,7 @@ class CalDAV(object):
att_data = []
exdates = []
+ _server_tzinfo = pytz.timezone(tools.get_server_timezone())
for cal_data in child.getChildren():
if cal_data.name.lower() == 'organizer':
@@ -329,8 +335,8 @@ class CalDAV(object):
if cal_data.name.lower() in self.__attribute__:
if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
- date_utc = cal_data.value.astimezone(pytz.utc)
- self.ical_set(cal_data.name.lower(), date_utc, 'value')
+ date_local = cal_data.value.astimezone(_server_tzinfo)
+ self.ical_set(cal_data.name.lower(), date_local, 'value')
continue
self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
vals = map_data(cr, uid, self, context=context)
@@ -576,6 +582,13 @@ class Calendar(CalDAV, osv.osv):
'create_date': fields.datetime('Created Date', readonly=True),
'write_date': fields.datetime('Modifided Date', readonly=True),
'description': fields.text("description"),
+ 'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
+ 'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
+ 'has_webcal': fields.boolean('WebCal', required=True, help="Also export a .ics entry next to the calendar folder, with WebCal content."),
+ }
+
+ _defaults = {
+ 'has_webcal': False,
}
def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
@@ -592,7 +605,7 @@ class Calendar(CalDAV, osv.osv):
continue
if line.name in ('valarm', 'attendee'):
continue
- line_domain = eval(line.domain or '[]')
+ line_domain = eval(line.domain or '[]', context)
line_domain += domain
if ctx_res_id:
line_domain += [('id','=',ctx_res_id)]
@@ -606,6 +619,32 @@ class Calendar(CalDAV, osv.osv):
node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
res.append(node)
return res
+
+
+ def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
+ if not context:
+ context = {}
+ if not domain:
+ domain = []
+ res = None
+ ctx_res_id = context.get('res_id', None)
+ ctx_model = context.get('model', None)
+ for cal in self.browse(cr, uid, ids):
+ for line in cal.line_ids:
+ if ctx_model and ctx_model != line.object_id.model:
+ continue
+ if line.name in ('valarm', 'attendee'):
+ continue
+ line_domain = eval(line.domain or '[]', context)
+ line_domain += domain
+ if ctx_res_id:
+ line_domain += [('id','=',ctx_res_id)]
+ mod_obj = self.pool.get(line.object_id.model)
+ max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
+ if res and res > max_data:
+ continue
+ res = max_data
+ return res
def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
""" Export Calendar
@@ -624,7 +663,7 @@ class Calendar(CalDAV, osv.osv):
continue
if line.name in ('valarm', 'attendee'):
continue
- domain = eval(line.domain or '[]')
+ domain = eval(line.domain or '[]', context)
if ctx_res_id:
domain += [('id','=',ctx_res_id)]
mod_obj = self.pool.get(line.object_id.model)
@@ -668,6 +707,15 @@ class Calendar(CalDAV, osv.osv):
val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
vals.append(val)
objs.append(cal_children[child.name.lower()])
+ elif child.name.upper() == 'CALSCALE':
+ if child.value.upper() != 'GREGORIAN':
+ self._logger.warning('How do I handle %s calendars?',child.value)
+ elif child.name.upper() in ('PRODID', 'VERSION'):
+ pass
+ elif child.name.upper().startswith('X-'):
+ self._logger.debug("skipping custom node %s", child.name)
+ else:
+ self._logger.debug("skipping node %s", child.name)
res = []
for obj_name in list(set(objs)):
@@ -728,6 +776,30 @@ line "%s" more than once' % (vals.get('name'))))
basic_calendar_line()
+class basic_calendar_alias(osv.osv):
+ """ Mapping of client filenames to ORM ids of calendar records
+
+ Since some clients insist on putting arbitrary filenames on the .ics data
+ they send us, and they won't respect the redirection "Location:" header,
+ we have to store those filenames and allow clients to call our calendar
+ records with them.
+ Note that adding a column to all tables that would possibly hold calendar-
+ mapped data won't work. The user is always allowed to specify more
+ calendars, on any arbitrary ORM object, without need to alter those tables'
+ data or structure
+ """
+ _name = 'basic.calendar.alias'
+ _columns = {
+ 'name': fields.char('Filename', size=512, required=True, select=1),
+ 'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
+ select=1, help='The calendar/line this mapping applies to'),
+ 'res_id': fields.integer('Res. ID', required=True, select=1),
+ }
+
+ _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
+ _('The same filename cannot apply to two records!')), ]
+
+basic_calendar_alias()
class basic_calendar_attribute(osv.osv):
_name = 'basic.calendar.attributes'
diff --git a/addons/caldav/calendar_collection.py b/addons/caldav/calendar_collection.py
index 8dd4ff053c9..52fe698647c 100644
--- a/addons/caldav/calendar_collection.py
+++ b/addons/caldav/calendar_collection.py
@@ -18,9 +18,11 @@
# along with this program. If not, see .
#
##############################################################################
+
from osv import osv, fields
from tools.translate import _
import caldav_node
+import logging
class calendar_collection(osv.osv):
_inherit = 'document.directory'
@@ -30,7 +32,8 @@ class calendar_collection(osv.osv):
}
_default = {
'calendar_collection' : False,
- }
+ }
+
def _get_root_calendar_directory(self, cr, uid, context=None):
objid = self.pool.get('ir.model.data')
try:
@@ -40,18 +43,25 @@ class calendar_collection(osv.osv):
root_id = objid.read(cr, uid, mid, ['res_id'])['res_id']
root_cal_dir = self.browse(cr,uid, root_id, context=context)
return root_cal_dir.name
- except Exception, e:
- import netsvc
- logger = netsvc.Logger()
- logger.notifyChannel("document", netsvc.LOG_WARNING, 'Cannot set root directory for Calendars:'+ str(e))
+ except Exception:
+ logger = logging.getLogger('document')
+ logger.warning('Cannot set root directory for Calendars:', exc_info=True)
return False
return False
- def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext):
- """ try to locate the node in uri,
- Return a tuple (node_dir, remaining_path)
- """
- return (caldav_node.node_database(context=ncontext), uri)
+ def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
+ if dbro is None:
+ dbro = self.browse(cr, uid, ids, context=context)
+
+ if dbro.calendar_collection:
+ if dynamic:
+ return caldav_node.node_calendar_res_col
+ else:
+ return caldav_node.node_calendar_collection
+ else:
+ return super(calendar_collection, self).\
+ get_node_class(cr, uid, ids, dbro=dbro,dynamic=dynamic,
+ context=context)
def get_description(self, cr, uid, ids, context=None):
#TODO : return description of all calendars
diff --git a/addons/caldav/doc/Path discovery.rst b/addons/caldav/doc/Path discovery.rst
new file mode 100644
index 00000000000..d2d434b5fb6
--- /dev/null
+++ b/addons/caldav/doc/Path discovery.rst
@@ -0,0 +1,43 @@
+===============================
+Discovery of calendar resources
+===============================
+
+1. Srv record
+--------------
+Calendar server and port should be advertised by a DNS _srv record.
+Although this is beyond the capabilities of the OpenERP server, an
+example setup is listed below:
+ -- TODO --
+
+DNS -> http://our-host-ip:port/
+
+2. Well-known uris
+-------------------
+The OpenERP server may have the 'well-known URIs' servlet activated,
+which means that it will advertise its main database and the correct
+location of the main CalDAV resource.
+http://our-host-ip:port/.well-known/caldav -> http://our-host-ip:port/webdav/dbname/calendars/
+
+
+3. Caldav collection
+---------------------
+The CalDAV "collection" is not necessarily a calendar or a folder just
+containing calendars under it. It is a DAV resource (aka folder) which
+has special DAV properties, so that clients are redirected to the right
+urls (like per-user calendars etc.).
+
+http://our-host-ip:port/webdav/dbname/calendars/ -> http://our-host-ip:port/webdav/dbname/calendars/users/user-login/c/
+
+4. Calendar home for user
+--------------------------
+There can be one dynamic folder per user, which will in turn contain the calendars
+
+http://our-host-ip:port/webdav/dbname/calendars/users/user-login/c/ ->
+http://our-host-ip:port/webdav/dbname/calendars/users/user-login/c/[Meetings, Tasks]
+
+5. Calendars
+--------------
+Each calendar will contain the resource nodes:
+ .../c/Meetings/ -> .../c/Meetings/123.ics
+
+Principal url
diff --git a/addons/caldav/doc/iPhone.rst b/addons/caldav/doc/iPhone.rst
new file mode 100644
index 00000000000..4a3af7724d2
--- /dev/null
+++ b/addons/caldav/doc/iPhone.rst
@@ -0,0 +1,97 @@
+==========================
+CalDAV with iPhone How-To
+==========================
+
+As from OpenERP v6.0, document_webdav v2.2, the iPhone has been thoroughly
+tested and supported as a Calendaring client for the OpenERP CalDAV module.
+
+However, keep in mind that OpenERP is not a straightforward calendaring
+server, but an ERP application (with more data + structure) which exposes
+that data to calendar clients. That said, the full features that would be
+accessible through the Gtk or Web OpenERP clients cannot be crammed into
+the Calendar clients (such as the iPhone).
+
+OpenERP server Setup
+--------------------
+Some modules need to be installed at the OpenERP server. These are:
+ - caldav: Required, has the reference setup and the necessary
+ underlying code. Will also cause document, document_webdav
+ to be installed.
+ - crm_caldav: Optional, will export the CRM Meetings as a calendar.
+ - project_caldav: Optional, will export project tasks as calendar.
+ - http_well_known: Optional, experimental. Will ease bootstrapping,
+ but only when a DNS srv record is also used.
+
+These will also install a reference setup of the folders, ready to go.
+The administrator of OpenERP can add more calendars and structure, if
+needed.
+
+DNS server setup
+------------------
+To be documented.
+
+SSL setup
+----------
+It is highly advisable that you also setup SSL to work for the OpenERP
+server. HTTPS is a server-wide feature in OpenERP, which means a
+certificate will be set at the openerp-server.conf and will be the same
+for XML-RPC, HTTP, WebDAV and CalDAV.
+The iPhone also supports secure connections with SSL, although it does
+not expect a self-signed certificate (or one that is not verified by
+one of the "big" certificate authorities [1] ).
+
+Phone setup
+-------------
+The iPhone is fairly easy to setup.
+IF you need SSL (and your certificate is not a verified one, as usual),
+then you first will need to let the iPhone trust that. Follow these
+steps:
+ s1. Open Safari and enter the https location of the OpenERP server:
+ https://my.server.ip:8071/
+ (assuming you have the server at "my.server.ip" and the HTTPS port
+ is the default 8071)
+ s2. Safari will try to connect and issue a warning about the certificate
+ used. Inspect the certificate and click "Accept" so that iPhone
+ now trusts it.
+
+Now, to setup the calendars, you need to:
+1. Click on the "Settings" and go to the "Mail, Contacts, Calendars" page.
+2. Go to "Add account..."
+3. Click on "Other"
+4. From the "Calendars" group, select "Add CalDAV Account"
+5. Enter the server's name or IP address at the "Server" entry, the
+ OpenERP username and password at the next ones.
+ As a description, you can either leave the server's name or
+ something like "OpenERP calendars".
+6. You _will_ get the "Unable to verify account" error message. That is
+ because our server is not at the port iPhone expects[2]. But no
+ need to worry, click OK.
+7. At the next page, enter the "Advanced Settings" to specify the right
+ ports and paths
+8. If you have SSL, turn the switch on. Note that port will be changed
+ to 8443.
+9. Specify the port for the OpenERP server: 8071 for SSL, 8069 without.
+10. Set the "Account URL" to the right path of the OpenERP webdav:
+ https://my.server.ip:8071/webdav/dbname/calendars
+ Where "https://my.server.ip:8071" is the protocol, server name
+ and port as discussed above, "dbname" is the name of the database.
+ [Note that the default
+ "https://my.server.ip:8071/principals/users/username" might also
+ be edited to
+ "https://my.server.ip:8071/webdav/dbname/principals/users/username" ]
+11. Click on Done. The phone will hopefully connect to the OpenERP server
+ and verify it can use the account.
+12. Go to the main menu of the iPhone and enter the Calendar application.
+ Your OpenERP calendars will be visible inside the selection of the
+ "Calendars" button.
+ Note that when creating a new calendar entry, you will have to specify
+ which calendar it should be saved at.
+
+
+
+
+[1] I remember one guy that made *lots* of money selling his CA business
+off, and since then uses this money to create a software monopoly.
+[2] This may not happen if the SRV records at DNS and the well-known URIs
+are setup right. But we appreciate that a default OpenERP installation will
+not have the DNS server of the company's domain configured.
diff --git a/addons/caldav/orm_utils.py b/addons/caldav/orm_utils.py
new file mode 100644
index 00000000000..ab0b4bbb8db
--- /dev/null
+++ b/addons/caldav/orm_utils.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010 OpenERP SA (www.openerp.com)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+
+if True: # we need this indentation level ;)
+
+ def get_last_modified(self, cr, user, args, context=None, access_rights_uid=None):
+ """Return the last modification date of objects in 'domain'
+ This function has similar semantics to orm.search(), apart from the
+ limit, offset and order arguments, which make no sense here.
+ It is useful when we want to find if the table (aka set of records)
+ has any modifications we should update at the client.
+ """
+ if context is None:
+ context = {}
+ self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context)
+
+ query = self._where_calc(cr, user, args, context=context)
+ self._apply_ir_rules(cr, user, query, 'read', context=context)
+ from_clause, where_clause, where_clause_params = query.get_sql()
+
+ where_str = where_clause and (" WHERE %s" % where_clause) or ''
+
+ cr.execute('SELECT MAX(COALESCE("%s".write_date, "%s".create_date)) FROM ' % (self._table, self._table) +
+ from_clause + where_str ,
+ where_clause_params)
+ res = cr.fetchall()
+ return res[0][0]
+
+#eof
diff --git a/addons/caldav/security/ir.model.access.csv b/addons/caldav/security/ir.model.access.csv
index bb16603b904..ce3f3c90e32 100644
--- a/addons/caldav/security/ir.model.access.csv
+++ b/addons/caldav/security/ir.model.access.csv
@@ -3,3 +3,4 @@
"access_basic_calendar_attributes","basic.calendar.attributes","model_basic_calendar_attributes","base.group_user",1,1,1,1
"access_basic_calendar_fields","basic.calendar.fields","model_basic_calendar_fields","base.group_user",1,1,1,1
"access_basic_calendar","basic.calendar","model_basic_calendar","base.group_user",1,1,1,1
+"access_basic_calendar_alias","basic.calendar.alias","model_basic_calendar_alias","base.group_user",1,1,1,1
diff --git a/addons/caldav/wizard/calendar_event_export.py b/addons/caldav/wizard/calendar_event_export.py
index ea90686b347..0a55f8b4fbe 100644
--- a/addons/caldav/wizard/calendar_event_export.py
+++ b/addons/caldav/wizard/calendar_event_export.py
@@ -37,8 +37,11 @@ class calendar_event_export(osv.osv_memory):
"""
Get Default value for Name field.
"""
- if not context:
+ if context is None:
context = {}
+ else:
+ context= context.copy()
+ context['uid'] = uid
model = context.get('model', 'basic.calendar')
model_obj = self.pool.get(model)
res = super(calendar_event_export, self).default_get( cr, uid, fields, context=context)
diff --git a/addons/caldav/wizard/calendar_event_import.py b/addons/caldav/wizard/calendar_event_import.py
index 875e48d7d47..11f9f9b4afb 100644
--- a/addons/caldav/wizard/calendar_event_import.py
+++ b/addons/caldav/wizard/calendar_event_import.py
@@ -42,6 +42,11 @@ class calendar_event_import(osv.osv_memory):
@param ids: List of calendar event import’s IDs
@return: dictionary of calendar evet import window with Import successful msg.
"""
+ if context is None:
+ context = {}
+ else:
+ context = context.copy()
+ context['uid'] = uid
for data in self.read(cr, uid, ids):
model = data.get('model', 'basic.calendar')
diff --git a/addons/caldav/wizard/calendar_event_subscribe.py b/addons/caldav/wizard/calendar_event_subscribe.py
index de673b5d7c0..f8dc7614507 100644
--- a/addons/caldav/wizard/calendar_event_subscribe.py
+++ b/addons/caldav/wizard/calendar_event_subscribe.py
@@ -44,6 +44,12 @@ class calendar_event_subscribe(osv.osv_memory):
@return: dictionary of calendar evet subscribe window with Import successful msg.
"""
global cnt
+ if context is None:
+ context = {}
+ else:
+ context = context.copy()
+ context['uid'] = uid
+
for data in self.read(cr, uid, ids):
try:
f = urllib.urlopen(data['url_path'])
diff --git a/addons/crm_caldav/__openerp__.py b/addons/crm_caldav/__openerp__.py
index ce8f72a3e17..d0e04576b3b 100644
--- a/addons/crm_caldav/__openerp__.py
+++ b/addons/crm_caldav/__openerp__.py
@@ -22,24 +22,24 @@
{
'name': 'Extened Module to Add CalDav future on Meeting',
- 'version': '1.0',
+ 'version': '1.1',
'category': 'Generic Modules/CRM & SRM',
'description': """
- New Futures in Meeting:
- * Share meeting with other calendar clients like sunbird
-""",
- 'author': 'OpenERP SA',
- 'website': 'http://www.openerp.com',
- 'depends': ['caldav', 'crm'
- ],
- 'init_xml': [
- 'crm_caldav_data.xml',
- ],
+ New Futures in Meeting:
+ * Share meeting with other calendar clients like sunbird
+""",
+ 'author': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['caldav', 'crm' ],
+ 'init_xml': [
+ 'crm_caldav_data.xml',
+ 'crm_caldav_setup.xml',
+ ],
'update_xml': [],
- 'demo_xml': [],
- 'installable': True,
- 'active': False,
+ 'demo_xml': [],
+ 'installable': True,
+ 'active': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/crm_caldav/crm_caldav.py b/addons/crm_caldav/crm_caldav.py
index 22f64f4d63d..1a2deae90be 100644
--- a/addons/crm_caldav/crm_caldav.py
+++ b/addons/crm_caldav/crm_caldav.py
@@ -77,7 +77,7 @@ class crm_meeting(osv.osv):
try:
for val in vals:
# Compute value of duration
- if 'date_deadline' in val and 'duration' not in val:
+ if val.get('date_deadline', False) and 'duration' not in val:
start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
diff = end - start
diff --git a/addons/crm_caldav/crm_caldav_setup.xml b/addons/crm_caldav/crm_caldav_setup.xml
new file mode 100644
index 00000000000..1a103f61811
--- /dev/null
+++ b/addons/crm_caldav/crm_caldav_setup.xml
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+ Meetings per user
+ #1E25FF
+
+ vevent
+ Meetings
+
+
+
+
+ []
+ attendee
+
+
+
+
+
+ []
+ valarm
+
+
+
+
+
+ [('user_id','=', dctx_user_id)]
+ vevent
+
+
+
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ {'tentative': 'draft', 'confirmed': 'open', 'cancelled': 'cancel'}
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
diff --git a/addons/document/__openerp__.py b/addons/document/__openerp__.py
index 16bbcd41c83..5741a7223e0 100644
--- a/addons/document/__openerp__.py
+++ b/addons/document/__openerp__.py
@@ -22,7 +22,7 @@
{
'name': 'Integrated Document Management System',
- 'version': '1.1',
+ 'version': '2.0',
'category': 'Generic Modules/Others',
'description': """This is a complete document management system:
* User Authentication
diff --git a/addons/document/content_index.py b/addons/document/content_index.py
index e3b37914f8c..df2b12b5456 100644
--- a/addons/document/content_index.py
+++ b/addons/document/content_index.py
@@ -21,11 +21,11 @@
import logging
import os
import tempfile
+from subprocess import Popen, PIPE
class NhException(Exception):
pass
-from subprocess import Popen, PIPE
class indexer(object):
""" An indexer knows how to parse the content of some file.
diff --git a/addons/document/dict_tools.py b/addons/document/dict_tools.py
new file mode 100644
index 00000000000..10d2fdc69f3
--- /dev/null
+++ b/addons/document/dict_tools.py
@@ -0,0 +1,57 @@
+# -*- encoding: utf-8 -*-
+# Copyright P. Christeas 2008-2010
+# Copyright 2010 OpenERP SA. http://www.openerp.com
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+###############################################################################
+
+
+def dict_merge(*dicts):
+ """ Return a dict with all values of dicts
+ """
+ res = {}
+ for d in dicts:
+ res.update(d)
+ return res
+
+def dict_merge2(*dicts):
+ """ Return a dict with all values of dicts.
+ If some key appears twice and contains iterable objects, the values
+ are merged (instead of overwritten).
+ """
+ res = {}
+ for d in dicts:
+ for k in d.keys():
+ if k in res and isinstance(res[k], (list, tuple)):
+ res[k] = res[k] + d[k]
+ elif k in res and isinstance(res[k], dict):
+ res[k].update(d[k])
+ else:
+ res[k] = d[k]
+ return res
+
+def dict_filter(srcdic, keys, res=None):
+ ''' Return a copy of srcdic that has only keys set.
+ If any of keys are missing from srcdic, the result won't have them,
+ either.
+ @param res If given, result will be updated there, instead of a new dict.
+ '''
+ if res is None:
+ res = {}
+ for k in keys:
+ if k in srcdic:
+ res[k] = srcdic[k]
+ return res
+
+#eof
diff --git a/addons/document/document.py b/addons/document/document.py
index 92a00f3222c..470d67c582b 100644
--- a/addons/document/document.py
+++ b/addons/document/document.py
@@ -28,6 +28,7 @@ import os
import tools
from tools.translate import _
import nodes
+import logging
DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config['root_path'], 'filestore'))
@@ -106,7 +107,7 @@ class document_file(osv.osv):
'parent_id': __get_def_directory
}
_sql_constraints = [
- ('filename_uniq', 'unique (name,parent_id,res_id,res_model)', 'The file name must be unique !')
+ # filename_uniq is not possible in pure SQL
]
def _check_duplication(self, cr, uid, vals, ids=[], op='create'):
name = vals.get('name', False)
@@ -259,6 +260,9 @@ class document_file(osv.osv):
r = stor.prepare_unlink(cr, uid, storage_id, f)
if r:
unres.append(r)
+ else:
+ logging.getLogger('document').warning("Unlinking attachment #%s %s that has no storage",
+ f.id, f.name)
res = super(document_file, self).unlink(cr, uid, ids, context)
stor.do_unlink(cr, uid, unres)
return res
diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py
index 47cea1e86bf..9b7ba18b16f 100644
--- a/addons/document/document_directory.py
+++ b/addons/document/document_directory.py
@@ -125,6 +125,10 @@ class document_directory(osv.osv):
def get_full_path(self, cr, uid, dir_id, context=None):
""" Return the full path to this directory, in a list, root first
"""
+ if isinstance(dir_id, (tuple, list)):
+ assert len(dir_id) == 1
+ dir_id = dir_id[0]
+
def _parent(dir_id, path):
parent=self.browse(cr, uid, dir_id)
if parent.parent_id and not parent.ressource_parent_type_id:
@@ -174,6 +178,39 @@ class document_directory(osv.osv):
return nodes.get_node_context(cr, uid, context).get_uri(cr, uri)
+ def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
+ """Retrieve the class of nodes for this directory
+
+ This function can be overriden by inherited classes ;)
+ @param dbro The browse object, if caller already has it
+ """
+ if dbro is None:
+ dbro = self.browse(cr, uid, ids, context=context)
+
+ if dynamic:
+ assert dbro.type == 'directory'
+ return nodes.node_res_obj
+ elif dbro.type == 'directory':
+ return nodes.node_dir
+ elif dbro.type == 'ressource':
+ return nodes.node_res_dir
+ else:
+ raise ValueError("dir node for %s type", dbro.type)
+
+ def _prepare_context(self, cr, uid, nctx, context):
+ """ Fill nctx with properties for this database
+ @param nctx instance of nodes.node_context, to be filled
+ @param context ORM context (dict) for us
+
+ Note that this function is called *without* a list of ids,
+ it should behave the same for the whole database (based on the
+ ORM instance of document.directory).
+
+ Some databases may override this and attach properties to the
+ node_context. See WebDAV, CalDAV.
+ """
+ return
+
def get_dir_permissions(self, cr, uid, ids ):
"""Check what permission user 'uid' has on directory 'id'
"""
@@ -215,6 +252,7 @@ class document_directory(osv.osv):
name=directory.name
if not parent_id:
parent_id=directory.parent_id and directory.parent_id.id or False
+ # TODO fix algo
if not ressource_parent_type_id:
ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
if not ressource_id:
@@ -235,8 +273,11 @@ class document_directory(osv.osv):
def create(self, cr, uid, vals, context=None):
if not self._check_duplication(cr, uid, vals):
raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
- if vals.get('name',False) and (vals.get('name').find('/')+1 or vals.get('name').find('@')+1 or vals.get('name').find('$')+1 or vals.get('name').find('#')+1) :
- raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
+ newname = vals.get('name',False)
+ if newname:
+ for illeg in ('/', '@', '$', '#'):
+ if illeg in newname:
+ raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
return super(document_directory,self).create(cr, uid, vals, context)
# TODO def unlink(...
@@ -255,7 +296,7 @@ class document_directory_dctx(osv.osv):
_name = 'document.directory.dctx'
_description = 'Directory Dynamic Context'
_columns = {
- 'dir_id': fields.many2one('document.directory', 'Directory', required=True),
+ 'dir_id': fields.many2one('document.directory', 'Directory', required=True, ondelete="cascade"),
'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field. Note that the prefix \"dctx_\" will be prepended to what is typed here."),
'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
"You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
diff --git a/addons/document/nodes.py b/addons/document/nodes.py
index b849396498d..68220885dff 100644
--- a/addons/document/nodes.py
+++ b/addons/document/nodes.py
@@ -61,16 +61,27 @@ def get_node_context(cr, uid, context):
return node_context(cr, uid, context)
class node_context(object):
- """ This is the root node, representing access to some particular
- context """
+ """ This is the root node, representing access to some particular context
+
+ A context is a set of persistent data, which may influence the structure
+ of the nodes. All other transient information during a data query should
+ be passed down with function arguments.
+ """
cached_roots = {}
+ node_file_class = None
def __init__(self, cr, uid, context=None):
self.dbname = cr.dbname
self.uid = uid
self.context = context
+ if context is None:
+ context = {}
+ context['uid'] = uid
self._dirobj = pooler.get_pool(cr.dbname).get('document.directory')
+ self.node_file_class = node_file
+ self.extra_ctx = {} # Extra keys for context, that do _not_ trigger inequality
assert self._dirobj
+ self._dirobj._prepare_context(cr, uid, self, context=context)
self.rootdir = False #self._dirobj._get_root_directory(cr,uid,context)
def __eq__(self, other):
@@ -88,10 +99,14 @@ class node_context(object):
def __ne__(self, other):
return not self.__eq__(other)
+
+ def get(self, name, default=None):
+ return self.context.get(name, default)
def get_uri(self, cr, uri):
""" Although this fn passes back to doc.dir, it is needed since
- it is a potential caching point """
+ it is a potential caching point.
+ """
(ndir, duri) = self._dirobj._locate_child(cr, self.uid, self.rootdir, uri, None, self)
while duri:
ndir = ndir.child(cr, duri[0])
@@ -104,14 +119,10 @@ class node_context(object):
"""Create (or locate) a node for a directory
@param dbro a browse object of document.directory
"""
- fullpath = self._dirobj.get_full_path(cr, self.uid, dbro.id, self.context)
- if dbro.type == 'directory':
- return node_dir(fullpath, None ,self, dbro)
- elif dbro.type == 'ressource':
- assert dbro.ressource_parent_type_id == False
- return node_res_dir(fullpath, None, self, dbro)
- else:
- raise ValueError("dir node for %s type", dbro.type)
+
+ fullpath = dbro.get_full_path(context=self.context)
+ klass = dbro.get_node_class(dbro, context=self.context)
+ return klass(fullpath, None ,self, dbro)
def get_file_node(self, cr, fbro):
""" Create or locate a node for a static file
@@ -121,7 +132,7 @@ class node_context(object):
if fbro.parent_id:
parent = self.get_dir_node(cr, fbro.parent_id)
- return node_file(fbro.name, parent, self, fbro)
+ return self.node_file_class(fbro.name, parent, self, fbro)
class node_descriptor(object):
@@ -266,22 +277,24 @@ class node_class(object):
""" Get a tag, unique per object + modification.
see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
- return self._get_ttag(cr) + ':' + self._get_wtag(cr)
+ return '"%s-%s"' % (self._get_ttag(cr), self._get_wtag(cr))
def _get_wtag(self, cr):
""" Return the modification time as a unique, compact string """
- return str(_str2time(self.write_date))
+ return str(_str2time(self.write_date)).replace('.','')
- def _get_ttag(self,cr):
+ def _get_ttag(self, cr):
""" Get a unique tag for this type/id of object.
Must be overriden, so that each node is uniquely identified.
"""
print "node_class.get_ttag()",self
- raise NotImplementedError("get_etag stub()")
+ raise NotImplementedError("get_ttag stub()")
def get_dav_props(self, cr):
""" If this class has special behaviour for GroupDAV etc, export
its capabilities """
+ # This fn is placed here rather than WebDAV, because we want the
+ # baseclass methods to apply to all node subclasses
return self.DAV_PROPS or {}
def match_dav_eprop(self, cr, match, ns, prop):
@@ -438,22 +451,20 @@ class node_database(node_class):
if not domain:
domain = []
- where2 = where + domain + [('type', '=', 'directory')]
+ where2 = where + domain + ['|', ('type', '=', 'directory'), \
+ '&', ('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)]
ids = dirobj.search(cr, uid, where2, context=ctx)
res = []
for dirr in dirobj.browse(cr, uid, ids, context=ctx):
- res.append(node_dir(dirr.name, self, self.context,dirr))
-
- where2 = where + domain + [('type', '=', 'ressource'), ('ressource_parent_type_id','=',False)]
- ids = dirobj.search(cr, uid, where2, context=ctx)
- for dirr in dirobj.browse(cr, uid, ids, context=ctx):
- res.append(node_res_dir(dirr.name, self, self.context, dirr))
+ klass = dirr.get_node_class(dirr, context=ctx)
+ res.append(klass(dirr.name, self, self.context,dirr))
fil_obj = dirobj.pool.get('ir.attachment')
ids = fil_obj.search(cr, uid, where, context=ctx)
if ids:
for fil in fil_obj.browse(cr, uid, ids, context=ctx):
- res.append(node_file(fil.name, self, self.context, fil))
+ klass = self.context.node_file_class
+ res.append(klass(fil.name, self, self.context, fil))
return res
def _file_get(self,cr, nodename=False):
@@ -463,9 +474,6 @@ class node_database(node_class):
def _get_ttag(self,cr):
return 'db-%s' % cr.dbname
- def get_dav_resourcetype(self, cr):
- return ('collection', 'DAV:')
-
def mkdosname(company_name, default='noname'):
""" convert a string to a dos-like name"""
if not company_name:
@@ -492,7 +500,10 @@ class node_dir(node_database):
self.write_date = dirr and (dirr.write_date or dirr.create_date) or False
self.content_length = 0
self.unixperms = 040750
- self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+ try:
+ self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+ except Exception:
+ self.uuser = 'nobody'
self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
self.uidperms = dirr.get_dir_permissions()
if dctx:
@@ -505,7 +516,7 @@ class node_dir(node_database):
for dfld in dirr.dctx_ids:
try:
self.dctx['dctx_' + dfld.field] = safe_eval(dfld.expr,dc2)
- except Exception,e:
+ except Exception:
print "Cannot eval %s" % dfld.expr
print e
pass
@@ -670,12 +681,14 @@ class node_dir(node_database):
return ret
class node_res_dir(node_class):
- """ A special sibling to node_dir, which does only contain dynamically
+ """ A folder containing dynamic folders
+ A special sibling to node_dir, which does only contain dynamically
created folders foreach resource in the foreign model.
All folders should be of type node_res_obj and merely behave like
node_dirs (with limited domain).
"""
our_type = 'collection'
+ res_obj_class = None
def __init__(self, path, parent, context, dirr, dctx=None ):
super(node_res_dir,self).__init__(path, parent, context)
self.dir_id = dirr.id
@@ -687,7 +700,10 @@ class node_res_dir(node_class):
self.write_date = dirr.write_date or dirr.create_date
self.content_length = 0
self.unixperms = 040750
- self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+ try:
+ self.uuser = (dirr.user_id and dirr.user_id.login) or 'nobody'
+ except Exception:
+ self.uuser = 'nobody'
self.ugroup = mkdosname(dirr.company_id and dirr.company_id.name, default='nogroup')
self.uidperms = dirr.get_dir_permissions()
self.res_model = dirr.ressource_type_id and dirr.ressource_type_id.model or False
@@ -782,17 +798,15 @@ class node_res_dir(node_class):
continue
# Yes! we can't do better but skip nameless records.
- res.append(node_res_obj(name, self.dir_id, self, self.context, self.res_model, bo))
+ res.append(self.res_obj_class(name, self.dir_id, self, self.context, self.res_model, bo))
return res
def _get_ttag(self,cr):
return 'rdir-%d' % self.dir_id
- def get_dav_resourcetype(self, cr):
- return ('collection', 'DAV:')
-
class node_res_obj(node_class):
- """ A special sibling to node_dir, which does only contain dynamically
+ """ A dynamically created folder.
+ A special sibling to node_dir, which does only contain dynamically
created folders foreach resource in the foreign model.
All folders should be of type node_res_obj and merely behave like
node_dirs (with limited domain).
@@ -828,7 +842,7 @@ class node_res_obj(node_class):
for fld,expr in self.dctx_dict.items():
try:
self.dctx[fld] = safe_eval(expr, dc2)
- except Exception,e:
+ except Exception:
print "Cannot eval %s for %s" % (expr, fld)
print e
pass
@@ -886,7 +900,8 @@ class node_res_obj(node_class):
return res
- def get_dav_props(self, cr):
+ def get_dav_props_DEPR(self, cr):
+ # Deprecated! (but document_ics must be cleaned, first)
res = {}
cntobj = self.context._dirobj.pool.get('document.directory.content')
uid = self.context.uid
@@ -899,7 +914,8 @@ class node_res_obj(node_class):
res['http://groupdav.org/'] = ('resourcetype',)
return res
- def get_dav_eprop(self, cr, ns, prop):
+ def get_dav_eprop_DEPR(self, cr, ns, prop):
+ # Deprecated!
if ns != 'http://groupdav.org/' or prop != 'resourcetype':
logger.warning("Who asked for %s:%s?" % (ns, prop))
return None
@@ -910,6 +926,7 @@ class node_res_obj(node_class):
where = [('directory_id','=',self.dir_id) ]
ids = cntobj.search(cr,uid,where,context=ctx)
for content in cntobj.browse(cr, uid, ids, context=ctx):
+ # TODO: remove relic of GroupDAV
if content.extension == '.ics': # FIXME: call the content class!
return ('vevent-collection','http://groupdav.org/')
return None
@@ -944,20 +961,21 @@ class node_res_obj(node_class):
res_name = getattr(bo, namefield)
if not res_name:
continue
- res.append(node_res_obj(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
+ # TODO Revise
+ klass = directory.get_node_class(directory, dynamic=True, context=ctx)
+ res.append(klass(res_name, self.dir_id, self, self.context, self.res_model, res_bo = bo))
where2 = where + [('parent_id','=',self.dir_id) ]
ids = dirobj.search(cr, uid, where2, context=ctx)
for dirr in dirobj.browse(cr, uid, ids, context=ctx):
if dirr.type == 'directory':
- res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
+ klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
+ res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
elif dirr.type == 'ressource':
# child resources can be controlled by properly set dctx
- res.append(node_res_dir(dirr.name,self,self.context, dirr, {'active_id': self.res_id}))
-
-
-
+ klass = dirr.get_node_class(dirr, context=ctx)
+ res.append(klass(dirr.name,self,self.context, dirr, {'active_id': self.res_id}))
fil_obj = dirobj.pool.get('ir.attachment')
if self.res_find_all:
@@ -967,7 +985,8 @@ class node_res_obj(node_class):
ids = fil_obj.search(cr, uid, where3, context=ctx)
if ids:
for fil in fil_obj.browse(cr, uid, ids, context=ctx):
- res.append(node_file(fil.name, self, self.context, fil))
+ klass = self.context.node_file_class
+ res.append(klass(fil.name, self, self.context, fil))
# Get Child Ressource Directories
@@ -979,9 +998,11 @@ class node_res_obj(node_class):
dirids = dirids + dirobj.search(cr,uid, where5)
for dirr in dirobj.browse(cr, uid, dirids, context=ctx):
if dirr.type == 'directory' and not dirr.parent_id:
- res.append(node_res_obj(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
+ klass = dirr.get_node_class(dirr, dynamic=True, context=ctx)
+ res.append(klass(dirr.name, dirr.id, self, self.context, self.res_model, res_bo = None, res_id = self.res_id))
if dirr.type == 'ressource':
- res.append(node_res_dir(dirr.name, self, self.context, dirr, {'active_id': self.res_id}))
+ klass = dirr.get_node_class(dirr, context=ctx)
+ res.append(klass(dirr.name, self, self.context, dirr, {'active_id': self.res_id}))
return res
def create_child_collection(self, cr, objname):
@@ -993,9 +1014,9 @@ class node_res_obj(node_class):
uid = self.context.uid
ctx = self.context.context.copy()
ctx.update(self.dctx)
- res_obj = dirobj.pool.get(self.context.context['res_model'])
+ res_obj = dirobj.pool.get(self.res_model)
- object2 = res_obj.browse(cr, uid, self.context.context['res_id']) or False
+ object2 = res_obj.browse(cr, uid, self.res_id) or False
obj = dirobj.browse(cr, uid, self.dir_id)
if obj and (obj.type == 'ressource') and not object2:
@@ -1038,7 +1059,8 @@ class node_res_obj(node_class):
fil_id = fil_obj.create(cr, uid, val, context=ctx)
fil = fil_obj.browse(cr, uid, fil_id, context=ctx)
- fnode = node_file(path, self, self.context, fil)
+ klass = self.context.node_file_class
+ fnode = klass(path, self, self.context, fil)
if data is not None:
fnode.set_data(cr, data, fil)
return fnode
@@ -1046,8 +1068,7 @@ class node_res_obj(node_class):
def _get_ttag(self,cr):
return 'rodir-%d-%d' % (self.dir_id, self.res_id)
- def get_dav_resourcetype(self, cr):
- return ('collection', 'DAV:')
+node_res_dir.res_obj_class = node_res_obj
class node_file(node_class):
our_type = 'file'
@@ -1069,7 +1090,10 @@ class node_file(node_class):
elif not parent.check_perms('w'):
self.uidperms = 4
- self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
+ try:
+ self.uuser = (fil.user_id and fil.user_id.login) or 'nobody'
+ except Exception:
+ self.uuser = 'nobody'
self.ugroup = mkdosname(fil.company_id and fil.company_id.name, default='nogroup')
# This only propagates the problem to get_data. Better
@@ -1183,9 +1207,6 @@ class node_file(node_class):
def _get_ttag(self,cr):
return 'file-%d' % self.file_id
- def get_dav_resourcetype(self, cr):
- return ''
-
def move_to(self, cr, ndir_node, new_name=False, fil_obj=None, ndir_obj=None, in_write=False):
if ndir_node and ndir_node.context != self.context:
raise NotImplementedError("Cannot move files between contexts")
@@ -1382,3 +1403,5 @@ class nodefd_content(StringIO, node_descriptor):
finally:
cr.close()
StringIO.close(self)
+
+#eof
\ No newline at end of file
diff --git a/addons/document/test/document_test2.yml b/addons/document/test/document_test2.yml
index 34e834a5306..65312eaf10d 100644
--- a/addons/document/test/document_test2.yml
+++ b/addons/document/test/document_test2.yml
@@ -12,7 +12,7 @@
I create a "Testing" folder where all the test data will go.
-
!record {model: document.directory, id: dir_tests }:
- name: 'Testing'
+ name: 'Testing (will be deleted!)'
parent_id: dir_root
-
I create an attachment into the root folder (w. empty fields, test that
@@ -52,7 +52,7 @@
I search the testing folder for attachments.
-
!python {model: ir.attachment}: |
- ids = self.search(cr, uid, [('parent_id.name','=', 'Testing'), ('name','=','Test renamed 2')])
+ ids = self.search(cr, uid, [('parent_id.name','=', 'Testing (will be deleted!)'), ('name','=','Test renamed 2')])
assert ids == [ ref("file_test2") ], ids
-
I create an attachment to a 3rd resource, eg. a res.country
@@ -70,7 +70,14 @@
ids = self.search(cr, uid, [('res_model', '=', 'res.country'), ('res_id', '=', ref("base.za"))])
assert ids == [ ref("attach_3rd")], ids
-
- I delete the attachment
+ I delete the attachments
-
!python {model: ir.attachment}: |
+ self.unlink(cr, uid, [ref('file_test2')])
self.unlink(cr, uid, [ref('attach_3rd')])
+-
+ I delete the tests folder
+-
+ !python {model: document.directory}: |
+ self.unlink(cr, uid, [ref('dir_tests')])
+ cr.commit()
diff --git a/addons/document_ftp/ftpserver/abstracted_fs.py b/addons/document_ftp/ftpserver/abstracted_fs.py
index f69a0bbbaad..9c16e7dde55 100644
--- a/addons/document_ftp/ftpserver/abstracted_fs.py
+++ b/addons/document_ftp/ftpserver/abstracted_fs.py
@@ -363,7 +363,6 @@ class abstracted_fs(object):
"""Remove the specified directory."""
cr, node, rem = datacr
assert node
- cr = self.get_node_cr(node)
node.rmcol(cr)
cr.commit()
diff --git a/addons/document_ftp/test/document_ftp_test2.yml b/addons/document_ftp/test/document_ftp_test2.yml
index 53d5a2b78bf..da55c353767 100644
--- a/addons/document_ftp/test/document_ftp_test2.yml
+++ b/addons/document_ftp/test/document_ftp_test2.yml
@@ -200,6 +200,11 @@
# TODO move
-
I remove the 'Test-Folder3'
+-
+ !python {model: ir.attachment}: |
+ from document_ftp import test_easyftp as te
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
+ ftp.rmd("Test-Folder3")
-
I check that test3.txt is removed.
-
@@ -228,3 +233,22 @@
-
I move the 200 files to 'Test-Folder2'
# TODO
+
+-
+ I delete the 200 files
+-
+ !python {model: ir.attachment}: |
+ from document_ftp import test_easyftp as te
+ from cStringIO import StringIO
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
+ # TODO speed
+ ftp.delete('test3.txt')
+ for i in range(0, 200):
+ ftp.delete('test-name%s.txt' %i)
+-
+ I remove the 'Test-Folder2'
+-
+ !python {model: ir.attachment}: |
+ from document_ftp import test_easyftp as te
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
+ ftp.rmd("Test-Folder2")
diff --git a/addons/document_ftp/test/document_ftp_test3.yml b/addons/document_ftp/test/document_ftp_test3.yml
index dbdab370b97..a51f259f44e 100644
--- a/addons/document_ftp/test/document_ftp_test3.yml
+++ b/addons/document_ftp/test/document_ftp_test3.yml
@@ -5,10 +5,10 @@
from document_ftp import test_easyftp as te
ftp = te.get_plain_ftp(timeout=1.0)
-
- I create a folder called '������������ ��������' in the server.
+ I create in the server a folder called 'Äïêéìáóôéêüò ÖÜêåëëïò'
-
!record {model: document.directory, id: dir_itests }:
- name: '������������ ��������'
+ name: 'Äïêéìáóôéêüò ÖÜêåëëïò'
parent_id: document.dir_root
-
And then I create another folder, under it, through FTP
@@ -16,67 +16,77 @@
!python {model: ir.attachment}: |
cr.commit()
from document_ftp import test_easyftp as te
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������')
- ftp.mkd("�������� ��� ����")
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò')
+ ftp.mkd("ÖÜêåëëïò áðü êÜôù")
-
I check that this folder exists at the server
-
- !assert {model: document.directory, id: , search: "[('name','=','�������� ��� ����')]" }:
+ !assert {model: document.directory, id: , search: "[('name','=','ÖÜêåëëïò áðü êÜôù')]" }:
- parent_id != False
-
- I login with FTP and check that '������������ ��������' is there
+ I login with FTP and check that 'Äïêéìáóôéêüò ÖÜêåëëïò' is there
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�������� ��� ����')
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù')
-
- I create a file named '������' into that folder
+ I create a file named 'ÄïêéìÞ' into that folder
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�������� ��� ����')
- fdata = StringIO('������� �� utf-8')
- ftp.storbinary('STOR ������.txt', fdata)
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù')
+ fdata = StringIO('êåßìåíï ìå utf-8')
+ ftp.storbinary('STOR ÄïêéìÞ.txt', fdata)
-
- I remove the '������.txt' file
+ I remove the 'ÄïêéìÞ.txt' file
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�������� ��� ����')
- ftp.delete('������.txt')
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù')
+ ftp.delete('ÄïêéìÞ.txt')
-
- I rename '�������� ��� ����' into '�����'
+ I rename 'ÖÜêåëëïò áðü êÜôù' into 'Üëëïò'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������')
- ftp.rename("�������� ��� ����", "�����")
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò')
+ ftp.rename("ÖÜêåëëïò áðü êÜôù", "Üëëïò")
-
- I place a file 'file �3' in '�����'
+ I place a file 'file Ö3' in 'Üëëïò'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����')
- fdata = StringIO('�� ���� �������')
- ftp.storbinary('STOR file �3.txt', fdata)
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
+ fdata = StringIO('êé Üëëï êåßìåíï')
+ ftp.storbinary('STOR file Ö3.txt', fdata)
-
I rename the file into file+range(1..200) (large filename)
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����')
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
vuvuzela = 'b'+''.join('z' * 200)+'!'
- ftp.rename("file �3.txt", vuvuzela)
+ ftp.rename("file Ö3.txt", vuvuzela)
-
I delete the file with the large name
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
- ftp = te.get_ftp_folder(cr, uid, self, 'Documents/������������ ��������/�����')
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
vuvuzela = 'b'+''.join('z' * 200)+'!'
ftp.delete(vuvuzela)
+
+-
+ I delete the testing folders
+-
+ !python {model: ir.attachment}: |
+ from document_ftp import test_easyftp as te
+ from cStringIO import StringIO
+ ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
+ ftp.rmd('Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
+ ftp.rmd('Äïêéìáóôéêüò ÖÜêåëëïò')
diff --git a/addons/document_ftp/test/document_ftp_test4.yml b/addons/document_ftp/test/document_ftp_test4.yml
index eeed4b17dda..2bb616dc1bd 100644
--- a/addons/document_ftp/test/document_ftp_test4.yml
+++ b/addons/document_ftp/test/document_ftp_test4.yml
@@ -118,3 +118,10 @@
- |
Bonus Piste:
I create a 'Partner3' under 'all'
+-
+ I delete the Partners Testing folder
+-
+ !python {model: document.directory}: |
+ self.unlink(cr, uid, [ref('dir_tests2')])
+ self.unlink(cr, uid, [ref('dir_respart1')])
+ cr.commit()
\ No newline at end of file
diff --git a/addons/document_webdav/__init__.py b/addons/document_webdav/__init__.py
index 69312668a74..ac417e18087 100644
--- a/addons/document_webdav/__init__.py
+++ b/addons/document_webdav/__init__.py
@@ -21,3 +21,7 @@
import webdav
import webdav_server
+
+import document_webdav
+
+#eof
\ No newline at end of file
diff --git a/addons/document_webdav/__openerp__.py b/addons/document_webdav/__openerp__.py
index 39330f68dea..1c2e232e914 100644
--- a/addons/document_webdav/__openerp__.py
+++ b/addons/document_webdav/__openerp__.py
@@ -29,20 +29,23 @@
##############################################################################
{
- "name" : "WebDAV server for Document Management",
- "version" : "2.0",
- "author" : "OpenERP SA",
- "category" : "Generic Modules/Others",
- "website": "http://www.openerp.com",
- "description": """ With this module, the WebDAV server for the documents is activated.
- You can then use any compatible browser to remotely see the attachments of OpenObject.
+ "name" : "WebDAV server for Document Management",
+ "version" : "2.2",
+ "author" : "OpenERP SA",
+ "category" : "Generic Modules/Others",
+ "website": "http://www.openerp.com",
+ "description": """ With this module, the WebDAV server for the documents is activated.
+ You can then use any compatible browser to remotely see the attachments of OpenObject.
- After installation, the webDAV server can be controlled by a [webdav] section in the server's config.
+ After installation, the webDAV server can be controlled by a [webdav] section in the server's config.
""",
- "depends" : ["base", "document"],
- "init_xml" : [],
- "update_xml" : [],
- "demo_xml" : [],
- "active": False,
- "installable": True
+ "depends" : ["base", "document"],
+ "init_xml" : [],
+ "update_xml" : ['security/ir.model.access.csv',
+ 'webdav_view.xml',
+ 'webdav_setup.xml',
+ ],
+ "demo_xml" : [],
+ "active": False,
+ "installable": True
}
diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py
index f3c8201bafc..3e8e1c2b3e5 100644
--- a/addons/document_webdav/dav_fs.py
+++ b/addons/document_webdav/dav_fs.py
@@ -26,8 +26,6 @@ import os
import time
from string import joinfields, split, lower
-from service import security
-
import netsvc
import urlparse
@@ -37,9 +35,13 @@ from DAV.iface import *
import urllib
from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
-from document.nodes import node_res_dir, node_res_obj
from cache import memoize
from tools import misc
+try:
+ from tools.dict_tools import dict_merge2
+except ImportError:
+ from document.dict_tools import dict_merge2
+
CACHE_SIZE=20000
#hack for urlparse: add webdav in the net protocols
@@ -96,8 +98,7 @@ class openerp_dav_handler(dav_interface):
return props
node = self.uri2object(cr, uid, pool, uri2)
if node:
- props = props.copy()
- props.update(node.get_dav_props(cr))
+ props = dict_merge2(props, node.get_dav_props(cr))
cr.close()
return props
@@ -134,11 +135,11 @@ class openerp_dav_handler(dav_interface):
self.parent.log_message("Exc: %s",traceback.format_exc())
raise default_exc("Operation failed")
- def _get_dav_lockdiscovery(self, uri):
- raise DAV_NotFound
+ #def _get_dav_lockdiscovery(self, uri):
+ # raise DAV_NotFound
- def _get_dav_supportedlock(self, uri):
- raise DAV_NotFound
+ #def A_get_dav_supportedlock(self, uri):
+ # raise DAV_NotFound
def match_prop(self, uri, match, ns, propname):
if self.M_NS.has_key(ns):
@@ -186,6 +187,16 @@ class openerp_dav_handler(dav_interface):
cr.close()
return ret
+ def reduce_useragent(self):
+ ua = self.parent.headers.get('User-Agent', False)
+ ctx = {}
+ if ua:
+ if 'iPhone' in ua:
+ ctx['DAV-client'] = 'iPhone'
+ elif 'Konqueror' in ua:
+ ctx['DAV-client'] = 'GroupDAV'
+ return ctx
+
def get_prop(self, uri, ns, propname):
""" return the value of a given property
@@ -194,17 +205,23 @@ class openerp_dav_handler(dav_interface):
pname -- name of the property
"""
if self.M_NS.has_key(ns):
- return dav_interface.get_prop(self, uri, ns, propname)
+ try:
+ # if it's not in the interface class, a "DAV:" property
+ # may be at the node class. So shouldn't give up early.
+ return dav_interface.get_prop(self, uri, ns, propname)
+ except DAV_NotFound:
+ pass
cr, uid, pool, dbname, uri2 = self.get_cr(uri)
if not dbname:
if cr: cr.close()
raise DAV_NotFound
- node = self.uri2object(cr, uid, pool, uri2)
- if not node:
+ try:
+ node = self.uri2object(cr, uid, pool, uri2)
+ if not node:
+ raise DAV_NotFound
+ res = node.get_dav_eprop(cr, ns, propname)
+ finally:
cr.close()
- raise DAV_NotFound
- res = node.get_dav_eprop(cr, ns, propname)
- cr.close()
return res
def get_db(self, uri, rest_ret=False, allow_last=False):
@@ -283,26 +300,55 @@ class openerp_dav_handler(dav_interface):
result = []
node = self.uri2object(cr, uid, pool, uri2[:])
- if not node:
- if cr: cr.close()
- raise DAV_NotFound2(uri2)
- else:
- fp = node.full_path()
- if fp and len(fp):
- self.parent.log_message('childs: @%s' % fp)
- fp = '/'.join(fp)
+ try:
+ if not node:
+ raise DAV_NotFound2(uri2)
else:
- fp = None
- domain = None
- if filters:
- domain = node.get_domain(cr, filters)
- for d in node.children(cr, domain):
- self.parent.log_message('child: %s' % d.path)
- if fp:
- result.append( self.urijoin(dbname,fp,d.path) )
+ fp = node.full_path()
+ if fp and len(fp):
+ fp = '/'.join(fp)
+ self.parent.log_message('childs for: %s' % fp)
else:
- result.append( self.urijoin(dbname,d.path) )
- if cr: cr.close()
+ fp = None
+ domain = None
+ if filters:
+ domain = node.get_domain(cr, filters)
+
+ if hasattr(filters, 'getElementsByTagNameNS'):
+ hrefs = filters.getElementsByTagNameNS('DAV:', 'href')
+ if hrefs:
+ ul = self.parent.davpath + self.uri2local(uri)
+ for hr in hrefs:
+ turi = ''
+ for tx in hr.childNodes:
+ if tx.nodeType == hr.TEXT_NODE:
+ turi += tx.data
+ if not turi.startswith('/'):
+ # it may be an absolute URL, decode to the
+ # relative part, because ul is relative, anyway
+ uparts=urlparse.urlparse(turi)
+ turi=uparts[2]
+ if turi.startswith(ul):
+ result.append( turi[len(self.parent.davpath):])
+ else:
+ self.parent.log_error("ignore href %s because it is not under request path %s", turi, ul)
+ return result
+ # We don't want to continue with the children found below
+ # Note the exceptions and that 'finally' will close the
+ # cursor
+ for d in node.children(cr, domain):
+ self.parent.log_message('child: %s' % d.path)
+ if fp:
+ result.append( self.urijoin(dbname,fp,d.path) )
+ else:
+ result.append( self.urijoin(dbname,d.path) )
+ except DAV_Error:
+ raise
+ except Exception, e:
+ self.parent.log_error("cannot get_childs: "+ str(e))
+ raise
+ finally:
+ if cr: cr.close()
return result
def uri2local(self, uri):
@@ -338,7 +384,8 @@ class openerp_dav_handler(dav_interface):
def uri2object(self, cr, uid, pool, uri):
if not uid:
return None
- return pool.get('document.directory').get_object(cr, uid, uri)
+ context = self.reduce_useragent()
+ return pool.get('document.directory').get_object(cr, uid, uri, context=context)
def get_data(self,uri, rrange=None):
self.parent.log_message('GET: %s' % uri)
diff --git a/addons/document_webdav/document_webdav.py b/addons/document_webdav/document_webdav.py
new file mode 100644
index 00000000000..28b70c8d7e4
--- /dev/null
+++ b/addons/document_webdav/document_webdav.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from osv import osv, fields
+import nodes
+from tools import config
+
+class document_davdir(osv.osv):
+ _inherit = 'document.directory'
+
+ _columns = {
+ # Placed here just for a reference
+ 'dav_prop_ids': fields.one2many('document.webdav.dir.property', 'dir_id', 'DAV properties'),
+ }
+
+ def get_node_class(self, cr, uid, ids, dbro=None, dynamic=False, context=None):
+ # Note: in this function, nodes come from document_webdav/nodes.py !
+ if dbro is None:
+ dbro = self.browse(cr, uid, ids, context=context)
+
+ if dynamic:
+ assert dbro.type == 'directory'
+ return nodes.node_res_obj
+ elif dbro.type == 'directory':
+ return nodes.node_dir
+ elif dbro.type == 'ressource':
+ return nodes.node_res_dir
+ else:
+ raise ValueError("dir node for %s type", dbro.type)
+
+ def _prepare_context(self, cr, uid, nctx, context):
+ nctx.node_file_class = nodes.node_file
+ # We can fill some more fields, but avoid any expensive function
+ # that might be not worth preparing.
+ nctx.extra_ctx['webdav_path'] = '/'+config.get_misc('webdav','vdir','webdav')
+ usr_obj = self.pool.get('res.users')
+ res = usr_obj.read(cr, uid, uid, ['login'])
+ if res:
+ nctx.extra_ctx['username'] = res['login']
+ # TODO group
+ return
+
+ def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext):
+ """ try to locate the node in uri,
+ Return a tuple (node_dir, remaining_path)
+ """
+ return (nodes.node_database(context=ncontext), uri)
+
+document_davdir()
+
+class dav_dir_property(osv.osv):
+ """ Arbitrary WebDAV properties, attached to document.directories.
+
+ Some DAV properties have to be settable at directories, depending
+ on the database directory structure.
+
+ Example would be the principal-URL.
+
+ There _can_ be properties without a directory, which means that they
+ globally apply to all the directories (aka. collections) of the
+ present database.
+ """
+ _name = 'document.webdav.dir.property'
+
+ _columns = {
+ 'dir_id': fields.many2one('document.directory', 'Directory', required=False, select=1),
+ 'namespace': fields.char('Namespace', size=127, required=True),
+ 'name': fields.char('Name', size=64, required=True),
+ 'value': fields.text('Value'),
+ 'do_subst': fields.boolean('Substitute', required=True),
+ }
+
+ _defaults = {
+ 'do_subst': False,
+ }
+
+dav_dir_property()
+
+#eof
\ No newline at end of file
diff --git a/addons/document_webdav/nodes.py b/addons/document_webdav/nodes.py
new file mode 100644
index 00000000000..8bea54ab7d3
--- /dev/null
+++ b/addons/document_webdav/nodes.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+
+from document import nodes
+from tools.safe_eval import safe_eval as eval
+try:
+ from tools.dict_tools import dict_filter
+except ImportError:
+ from document.dict_tools import dict_filter
+
+import urllib
+
+
+class node_acl_mixin(object):
+ def _get_dav_owner(self, cr):
+ return self.uuser
+
+ def _get_dav_group(self, cr):
+ return self.ugroup
+
+ def _get_dav_supported_privilege_set(self, cr):
+ return '' # TODO
+
+ def _get_dav_current_user_privilege_set(self, cr):
+ return '' # TODO
+
+ def _get_dav_props_hlpr(self, cr, par_class, prop_model,
+ prop_ref_field, res_id):
+ """ Helper for dav properties, usable in subclasses
+
+ @param par_class The parent class
+ @param prop_model The name of the orm model holding the properties
+ @param prop_ref_field The name of the field at prop_model pointing to us
+ @param res_id the id of self in the corresponing orm table, that should
+ match prop_model.prop_ref_field
+ """
+ ret = par_class.get_dav_props(self, cr)
+ if prop_model:
+ propobj = self.context._dirobj.pool.get(prop_model)
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ # Not really needed because we don't do eval here:
+ # ctx.update({'uid': uid, 'dbname': self.context.dbname })
+ # dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
+ sdomain = [(prop_ref_field, '=', False),]
+ if res_id:
+ sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain
+ prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
+ if prop_ids:
+ ret = ret.copy()
+ for pbro in propobj.browse(cr, uid, prop_ids, context=ctx):
+ ret[pbro.namespace] = ret.get(pbro.namespace, ()) + \
+ (pbro.name,)
+ # Note that we cannot have properties to conditionally appear
+ # on the context, yet.
+
+ return ret
+
+ def _get_dav_eprop_hlpr(self, cr, ns, prop,
+ par_class, prop_model,
+ prop_ref_field, res_id):
+ """ Helper for get dav eprop, usable in subclasses
+
+ @param namespace the one to search for
+ @param name Name to search for
+ @param par_class The parent class
+ @param prop_model The name of the orm model holding the properties
+ @param prop_ref_field The name of the field at prop_model pointing to us
+ @param res_id the id of self in the corresponing orm table, that should
+ match prop_model.prop_ref_field
+ """
+ ret = par_class.get_dav_eprop(self, cr, ns, prop)
+ if ret is not None:
+ return ret
+ if prop_model:
+ propobj = self.context._dirobj.pool.get(prop_model)
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ ctx.update({'uid': uid, 'dbname': self.context.dbname })
+ ctx['node_classname'] = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
+ dict_filter(self.context.extra_ctx, ['username', 'groupname', 'webdav_path'], ctx)
+ sdomain = [(prop_ref_field, '=', False),('namespace', '=', ns), ('name','=', prop)]
+ if res_id:
+ sdomain = ['|', (prop_ref_field, '=', res_id)] + sdomain
+ prop_ids = propobj.search(cr, uid, sdomain, context=ctx)
+ if prop_ids:
+ pbro = propobj.browse(cr, uid, prop_ids[0], context=ctx)
+ val = pbro.value
+ if pbro.do_subst:
+ if val.startswith("('") and val.endswith(")"):
+ glbls = { 'urlquote': urllib.quote, }
+ val = eval(val, glbls, ctx)
+ else:
+ val = val % ctx
+ return val
+ return None
+
+class node_dir(node_acl_mixin, nodes.node_dir):
+ """ override node_dir and add DAV functionality
+ """
+ DAV_PROPS = { "DAV:": ('owner', 'group',
+ 'supported-privilege-set',
+ 'current-user-privilege-set'),
+ }
+ DAV_M_NS = { "DAV:" : '_get_dav',}
+ http_options = { 'DAV': ['access-control',] }
+
+ def get_dav_resourcetype(self, cr):
+ return ('collection', 'DAV:')
+
+ def get_dav_props(self, cr):
+ return self._get_dav_props_hlpr(cr, nodes.node_dir,
+ 'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+ def get_dav_eprop(self, cr, ns, prop):
+ return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_dir,
+ 'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+
+class node_file(node_acl_mixin, nodes.node_file):
+ DAV_PROPS = { "DAV:": ('owner', 'group',
+ 'supported-privilege-set',
+ 'current-user-privilege-set'),
+ }
+ DAV_M_NS = { "DAV:" : '_get_dav',}
+ http_options = { 'DAV': ['access-control', ] }
+ pass
+
+ def get_dav_resourcetype(self, cr):
+ return ''
+
+ def get_dav_props(self, cr):
+ return self._get_dav_props_hlpr(cr, nodes.node_dir,
+ None, 'file_id', self.file_id)
+ #'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+ #def get_dav_eprop(self, cr, ns, prop):
+
+class node_database(nodes.node_database):
+ def get_dav_resourcetype(self, cr):
+ return ('collection', 'DAV:')
+
+ def get_dav_props(self, cr):
+ return self._get_dav_props_hlpr(cr, nodes.node_dir,
+ 'document.webdav.dir.property', 'dir_id', False)
+
+ def get_dav_eprop(self, cr, ns, prop):
+ return self._get_dav_eprop_hlpr(cr, nodes.node_dir, ns, prop,
+ 'document.webdav.dir.property', 'dir_id', False)
+
+class node_res_obj(node_acl_mixin, nodes.node_res_obj):
+ DAV_PROPS = { "DAV:": ('owner', 'group',
+ 'supported-privilege-set',
+ 'current-user-privilege-set'),
+ }
+ DAV_M_NS = { "DAV:" : '_get_dav',}
+ http_options = { 'DAV': ['access-control',] }
+
+ def get_dav_resourcetype(self, cr):
+ return ('collection', 'DAV:')
+
+ def get_dav_props(self, cr):
+ return self._get_dav_props_hlpr(cr, nodes.node_res_obj,
+ 'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+ def get_dav_eprop(self, cr, ns, prop):
+ return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_obj,
+ 'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+
+class node_res_dir(node_acl_mixin, nodes.node_res_dir):
+ DAV_PROPS = { "DAV:": ('owner', 'group',
+ 'supported-privilege-set',
+ 'current-user-privilege-set'),
+ }
+ DAV_M_NS = { "DAV:" : '_get_dav',}
+ http_options = { 'DAV': ['access-control',] }
+ res_obj_class = node_res_obj
+
+ def get_dav_resourcetype(self, cr):
+ return ('collection', 'DAV:')
+
+ def get_dav_props(self, cr):
+ return self._get_dav_props_hlpr(cr, nodes.node_res_dir,
+ 'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+ def get_dav_eprop(self, cr, ns, prop):
+ return self._get_dav_eprop_hlpr(cr, ns, prop, nodes.node_res_dir,
+ 'document.webdav.dir.property', 'dir_id', self.dir_id)
+
+# Some copies, so that this module can replace 'from document import nodes'
+get_node_context = nodes.get_node_context
+node_context = nodes.node_context
+node_class = nodes.node_class
+node_descriptor = nodes.node_descriptor
+
+
+#eof
diff --git a/addons/document_webdav/security/ir.model.access.csv b/addons/document_webdav/security/ir.model.access.csv
new file mode 100644
index 00000000000..a77304eb297
--- /dev/null
+++ b/addons/document_webdav/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_webdav_dir_property_all","webdav.dir.property all","model_document_webdav_dir_property",,1,0,0,0
+"access_webdav_dir_property_group_doc_manager","webdav.dir.property document manager","model_document_webdav_dir_property","base.group_system",1,1,1,1
+"access_webdav_dir_property_group_system","webdav.dir.property group system","model_document_webdav_dir_property","base.group_system",1,1,1,1
diff --git a/addons/document_webdav/webdav.py b/addons/document_webdav/webdav.py
index 3b6cf582067..bb2b8caf470 100644
--- a/addons/document_webdav/webdav.py
+++ b/addons/document_webdav/webdav.py
@@ -22,6 +22,8 @@
import xml.dom.minidom
domimpl = xml.dom.minidom.getDOMImplementation()
+from xml.dom.minicompat import StringTypes
+
import urlparse
import urllib
from osv import osv
@@ -29,12 +31,28 @@ from osv import osv
try:
from DAV import utils
from DAV.propfind import PROPFIND
+ from DAV.report import REPORT
except ImportError:
raise osv.except_osv('PyWebDAV Import Error!','Please install PyWebDAV \
from http://code.google.com/p/pywebdav/downloads/detail?name=PyWebDAV-0.9.4.tar.gz&can=2&q=/')
import tools
+class Text2(xml.dom.minidom.Text):
+ def writexml(self, writer, indent="", addindent="", newl=""):
+ data = "%s%s%s" % (indent, self.data, newl)
+ data = data.replace("&", "&").replace("<", "<")
+ data = data.replace(">", ">")
+ writer.write(data)
+
+def createText2Node(doc, data):
+ if not isinstance(data, StringTypes):
+ raise TypeError, "node contents must be a string"
+ t = Text2()
+ t.data = data
+ t.ownerDocument = doc
+ return t
+
super_mk_prop_response = PROPFIND.mk_prop_response
def mk_prop_response(self, uri, good_props, bad_props, doc):
@@ -48,7 +66,7 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
re=doc.createElement("D:response")
# append namespaces to response
nsnum=0
- namespaces = self.namespaces
+ namespaces = self.namespaces[:]
if 'DAV:' in namespaces:
namespaces.remove('DAV:')
for nsname in namespaces:
@@ -67,6 +85,8 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
string: text node
tuple ('elem', 'ns') for empty sub-node
tuple ('elem', 'ns', sub-elems) for sub-node with elements
+ tuple ('elem', 'ns', sub-elems, {attrs}) for sub-node with
+ optional elements and attributes
list, of above tuples
"""
if ns == 'DAV:':
@@ -108,13 +128,21 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
ve=doc.createElement(ns_prefix+v[0])
if need_ns:
ve.setAttribute("xmlns:ns"+str(nsnum), v[1])
- if len(v) > 2 and isinstance(v[2], list):
- # support nested elements like:
- # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...]
- _prop_elem_child(ve, v[1], v[2], ns_prefix)
+ if len(v) > 2 and v[2] is not None:
+ if isinstance(v[2], (list, tuple)):
+ # support nested elements like:
+ # ( 'elem', 'ns:', [('sub-elem1', 'ns1'), ...]
+ _prop_elem_child(ve, v[1], v[2], ns_prefix)
+ else:
+ vt=createText2Node(doc,tools.ustr(v[2]))
+ ve.appendChild(vt)
+ if len(v) > 3 and v[3]:
+ assert isinstance(v[3], dict)
+ for ak, av in v[3].items():
+ ve.setAttribute(ak, av)
pnode.appendChild(ve)
else:
- ve=doc.createTextNode(tools.ustr(v))
+ ve=createText2Node(doc, tools.ustr(v))
pnode.appendChild(ve)
# write href information
@@ -124,7 +152,12 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
fileloc = fileloc.encode('utf-8')
href=doc.createElement("D:href")
davpath = self._dataclass.parent.get_davpath()
- hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
+ if uparts[0] and uparts[1]:
+ hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
+ else:
+ # When the request has been relative, we don't have enough data to
+ # reply with absolute url here.
+ hurl = '%s%s' % (davpath, urllib.quote(fileloc))
huri=doc.createTextNode(hurl)
href.appendChild(huri)
re.appendChild(href)
@@ -133,6 +166,10 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
ps=doc.createElement("D:propstat")
if good_props:
re.appendChild(ps)
+ s=doc.createElement("D:status")
+ t=doc.createTextNode("HTTP/1.1 200 OK")
+ s.appendChild(t)
+ ps.appendChild(s)
gp=doc.createElement("D:prop")
for ns in good_props.keys():
@@ -141,15 +178,11 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
else:
ns_prefix="ns"+str(namespaces.index(ns))+":"
for p,v in good_props[ns].items():
- if not v:
+ if v is None:
continue
_prop_child(gp, ns, p, v)
ps.appendChild(gp)
- s=doc.createElement("D:status")
- t=doc.createTextNode("HTTP/1.1 200 OK")
- s.appendChild(t)
- ps.appendChild(s)
re.appendChild(ps)
# now write the errors!
@@ -159,6 +192,10 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
for ecode in bad_props.keys():
ps=doc.createElement("D:propstat")
re.appendChild(ps)
+ s=doc.createElement("D:status")
+ t=doc.createTextNode(utils.gen_estring(ecode))
+ s.appendChild(t)
+ ps.appendChild(s)
bp=doc.createElement("D:prop")
ps.appendChild(bp)
@@ -172,10 +209,6 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
pe=doc.createElement(ns_prefix+str(p))
bp.appendChild(pe)
- s=doc.createElement("D:status")
- t=doc.createTextNode(utils.gen_estring(ecode))
- s.appendChild(t)
- ps.appendChild(s)
re.appendChild(ps)
# return the new response element
@@ -198,7 +231,12 @@ def mk_propname_response(self,uri,propnames,doc):
fileloc = fileloc.encode('utf-8')
href=doc.createElement("D:href")
davpath = self._dataclass.parent.get_davpath()
- hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
+ if uparts[0] and uparts[1]:
+ hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
+ else:
+ # When the request has been relative, we don't have enough data to
+ # reply with absolute url here.
+ hurl = '%s%s' % (davpath, urllib.quote(fileloc))
huri=doc.createTextNode(hurl)
href.appendChild(huri)
re.appendChild(href)
@@ -230,3 +268,18 @@ def mk_propname_response(self,uri,propnames,doc):
PROPFIND.mk_prop_response = mk_prop_response
PROPFIND.mk_propname_response = mk_propname_response
+super_create_prop = REPORT.create_prop
+
+def create_prop(self):
+ try:
+ if (self.filter is not None) and self._depth == "0":
+ hrefs = self.filter.getElementsByTagNameNS('DAV:', 'href')
+ if hrefs:
+ self._depth = "1"
+ except Exception:
+ pass
+ return super_create_prop(self)
+
+REPORT.create_prop = create_prop
+
+#eof
diff --git a/addons/document_webdav/webdav_demo.xml b/addons/document_webdav/webdav_demo.xml
new file mode 100644
index 00000000000..45cd17ed288
--- /dev/null
+++ b/addons/document_webdav/webdav_demo.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ debug:
+ node_class
+ %(node_classname)s
+
+
+
+
+
+
+
diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py
index 44592243480..df2c4a871f6 100644
--- a/addons/document_webdav/webdav_server.py
+++ b/addons/document_webdav/webdav_server.py
@@ -57,7 +57,7 @@ def OpenDAVConfig(**kw):
class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
verbose = False
protocol_version = 'HTTP/1.1'
- _HTTP_OPTIONS= { 'DAV' : ['1',],
+ _HTTP_OPTIONS= { 'DAV' : ['1', '2'],
'Allow' : [ 'GET', 'HEAD', 'COPY', 'MOVE', 'POST', 'PUT',
'PROPFIND', 'PROPPATCH', 'OPTIONS', 'MKCOL',
'DELETE', 'TRACE', 'REPORT', ]
@@ -81,7 +81,15 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
def setup(self):
self.davpath = '/'+config.get_misc('webdav','vdir','webdav')
- self.baseuri = "http://%s:%d/"% (self.server.server_name, self.server.server_port)
+ addr, port = self.server.server_name, self.server.server_port
+ server_proto = getattr(self.server,'proto', 'http').lower()
+ try:
+ if hasattr(self.request, 'getsockname'):
+ addr, port = self.request.getsockname()
+ except Exception, e:
+ self.log_error("Cannot calculate own address: %s" , e)
+ # Too early here to use self.headers
+ self.baseuri = "%s://%s:%d/"% (server_proto, addr, port)
self.IFACE_CLASS = openerp_dav_handler(self, self.verbose)
def copymove(self, CLASS):
@@ -156,8 +164,6 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
etag = None
for match in self.headers['If-Match'].split(','):
- if match.startswith('"') and match.endswith('"'):
- match = match[1:-1]
if match == '*':
if dc.exists(uri):
test = True
@@ -208,8 +214,9 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler):
if self.headers.has_key("Content-Type"):
ct=self.headers['Content-Type']
try:
- location = dc.put(uri,body,ct)
+ location = dc.put(uri, body, ct)
except DAV_Error, (ec,dd):
+ self._logger.warning("Cannot PUT to %s: %s", uri, dd, exc_info=True)
return self.send_status(ec)
headers = {}
@@ -264,8 +271,7 @@ try:
handler.debug = config.get_misc('webdav','debug',True)
_dc = { 'verbose' : verbose,
'directory' : directory,
- 'lockemulation' : False,
-
+ 'lockemulation' : True,
}
conf = OpenDAVConfig(**_dc)
diff --git a/addons/document_webdav/webdav_setup.xml b/addons/document_webdav/webdav_setup.xml
new file mode 100644
index 00000000000..2bd41ddb69c
--- /dev/null
+++ b/addons/document_webdav/webdav_setup.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+ []
+
+
+
+
+
+ directory
+
+ principals
+
+
+
+ []
+
+
+
+
+
+
+ directory
+
+ groups
+
+
+
+ []
+
+
+
+
+
+ directory
+
+ resources
+
+
+ []
+
+
+
+
+
+
+ ressource
+
+
+ __uids__
+
+
+ []
+
+
+
+
+
+
+
+ ressource
+
+
+ users
+
+
+ []
+
+
+
+
+
+ directory
+
+ locations
+
+
+ DAV:
+ current-user-principal
+ ('href','DAV:','/%s/%s/principals/users/%s' % ('webdav',dbname, username ) )
+
+
+
+ DAV:
+ principal-URL
+ ('href','DAV:','/%s/%s/principals/users/%s' % ('webdav',dbname, username ) )
+
+
+
+
+
diff --git a/addons/document_webdav/webdav_view.xml b/addons/document_webdav/webdav_view.xml
new file mode 100644
index 00000000000..2ea0bca5279
--- /dev/null
+++ b/addons/document_webdav/webdav_view.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+ document.webdav.dir.property.form
+ document.webdav.dir.property
+ form
+
+
+
+
+
+
+ document.webdav.dir.property.tree
+ document.webdav.dir.property
+ tree
+
+
+
+
+
+
+
+
+
+
+ Search View: Directory DAV properties
+ document.webdav.dir.property
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ir.actions.act_window
+ document.webdav.dir.property
+ form
+ tree,form
+
+
+
+
+
+
+ document.directory.webdav.inherit
+ document.directory
+
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/project_caldav/__openerp__.py b/addons/project_caldav/__openerp__.py
index 1e78db79bf0..f298fc9e1d5 100644
--- a/addons/project_caldav/__openerp__.py
+++ b/addons/project_caldav/__openerp__.py
@@ -20,13 +20,13 @@
##############################################################################
{
- "name": "Caldav task management",
- "version": "1.0",
+ "name": "CalDAV for task management",
+ "version": "1.1",
"author": "OpenERP SA",
"category": "Generic Modules/Others",
"description": """ Synchronize between Project task and Caldav Vtodo.""",
"depends": ["project", "caldav", "base_calendar"],
- "init_xml": ["project_caldav_data.xml"],
+ "init_xml": ["project_caldav_data.xml", 'project_caldav_setup.xml', ],
"demo_xml": [],
"update_xml": ["project_caldav_view.xml"],
"active": False,
diff --git a/addons/project_caldav/project_caldav_data.xml b/addons/project_caldav/project_caldav_data.xml
index 0a4d36580fc..3030b28dba9 100644
--- a/addons/project_caldav/project_caldav_data.xml
+++ b/addons/project_caldav/project_caldav_data.xml
@@ -3,10 +3,10 @@
- Tasks
-
- vtodo
-
+ Tasks
+
+ vtodo
+
vtodo
diff --git a/addons/project_caldav/project_caldav_setup.xml b/addons/project_caldav/project_caldav_setup.xml
new file mode 100644
index 00000000000..96d9c0ad3b6
--- /dev/null
+++ b/addons/project_caldav/project_caldav_setup.xml
@@ -0,0 +1,272 @@
+
+
+
+
+
+ Tasks per user
+ #FFEF74
+
+ vtodo
+ Tasks
+
+
+
+
+ []
+ attendee
+
+
+
+
+ []
+ valarm
+
+
+
+
+ [('user_id','=', dctx_user_id)]
+ vtodo
+
+
+
+
+
+
+
+ {'needs-action': 'draft', 'completed': 'done', 'in-process': 'open', 'cancelled': 'cancelled'}
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ {'1': '0', '2': '1', '3': '1','4': '1', '5': '2', '6': '3', '7': '3', '8': '3', '9': '4'}
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ hours
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
+
+ field
+
+
+
+
\ No newline at end of file