diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py index df2d43d3a53..a1213dd7542 100644 --- a/addons/base_action_rule/base_action_rule.py +++ b/addons/base_action_rule/base_action_rule.py @@ -33,20 +33,105 @@ class base_action_rule(osv.osv): _name = 'base.action.rule' _description = 'Action Rules' + + def _state_get(self, cr, uid, context={}): + """ Get State + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values """ + + return self.state_get(cr, uid, context=context) + + + def state_get(self, cr, uid, context={}): + """ Get State + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values """ + + return [('', '')] + + def priority_get(self, cr, uid, context={}): + """ Get Priority + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values """ + + return [('', '')] _columns = { - 'name': fields.many2one('ir.model', 'Model', required=True), - 'max_level': fields.integer('Max Level', help='Specifies maximum level.'), - 'rule_lines': fields.one2many('base.action.rule.line','rule_id','Rule Lines'), - 'create_date': fields.datetime('Create Date', readonly=1), - 'active': fields.boolean('Active') + 'name': fields.many2one('ir.model', 'Object', required=True), + 'max_level': fields.integer('Max Level', help='Specifies maximum level.'), + 'create_date': fields.datetime('Create Date', readonly=1), + 'active': fields.boolean('Active', help="If the active field is set to true,\ + it will allow you to hide the rule without removing it."), + 'sequence': fields.integer('Sequence', help="Gives the sequence order \ +when displaying a list of rules."), + 'trg_date_type': fields.selection([ + ('none', 'None'), + ('create', 'Creation Date'), + ('action_last', 'Last Action Date'), + ('date', 'Date'), + ('deadline', 'Deadline'), + ], 'Trigger Date', size=16), + 'trg_date_range': fields.integer('Delay after trigger date', \ + help="Delay After Trigger Date,\ +specifies you can put a negative number. If you need a delay before the \ +trigger date, like sending a reminder 15 minutes before a meeting."), + 'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'), \ + ('day', 'Days'), ('month', 'Months')], 'Delay type'), + + + 'trg_user_id': fields.many2one('res.users', 'Responsible'), + + 'trg_partner_id': fields.many2one('res.partner', 'Partner'), + 'trg_partner_categ_id': fields.many2one('res.partner.category', 'Partner Category'), + 'trg_state_from': fields.selection(_state_get, 'State', size=16), + 'trg_state_to': fields.selection(_state_get, 'Button Pressed', size=16), + + 'act_method': fields.char('Call Object Method', size=64), + 'act_user_id': fields.many2one('res.users', 'Set responsible to'), + 'act_state': fields.selection(_state_get, 'Set state to', size=16), + 'act_email_cc': fields.char('Add watchers (Cc)', size=250, help="\ +These people will receive a copy of the future communication between partner \ +and users by email"), + 'act_remind_partner': fields.boolean('Remind Partner', help="Check \ +this if you want the rule to send a reminder by email to the partner."), + 'act_remind_user': fields.boolean('Remind responsible', help="Check \ +this if you want the rule to send a reminder by email to the user."), + 'act_reply_to': fields.char('Reply-To', size=64), + 'act_remind_attach': fields.boolean('Remind with attachment', help="Check this if you want that all documents attached to the object be attached to the reminder email sent."), + 'act_mail_to_user': fields.boolean('Mail to responsible', help="Check\ + this if you want the rule to send an email to the responsible person."), + 'act_mail_to_watchers': fields.boolean('Mail to watchers (CC)', + help="Check this if you want \ +the rule to mark CC(mail to any other person defined in actions)."), + 'act_mail_to_email': fields.char('Mail to these emails', size=128, \ + help="Email-id of the persons whom mail is to be sent"), + 'act_mail_body': fields.text('Mail body', help="Content of mail"), + 'regex_name': fields.char('Regular Expression on Model Name', size=128), + 'server_action_id': fields.many2one('ir.actions.server', 'Server Action', help="Describes the action name.\neg:on which object which action to be taken on basis of which condition"), + 'filter_id':fields.many2one('ir.filters', 'Filter', required=False), + 'domain':fields.char('Domain', size=124, required=False, readonly=False), } _defaults = { - 'active': lambda *a: True, - 'max_level': lambda *a: 15, + 'active': lambda *a: True, + 'max_level': lambda *a: 15, + 'trg_date_type': lambda *a: 'none', + 'trg_date_range_type': lambda *a: 'day', + 'act_mail_to_user': lambda *a: 0, + 'act_remind_partner': lambda *a: 0, + 'act_remind_user': lambda *a: 0, + 'act_mail_to_watchers': lambda *a: 0, + 'domain': lambda *a: '[]' } - + + _order = 'sequence' + def format_body(self, body): """ Foramat Action rule's body @param self: The object pointer """ @@ -58,22 +143,22 @@ class base_action_rule(osv.osv): @param self: The object pointer """ data = { - 'object_id': obj.id, - 'object_subject': hasattr(obj, 'name') and obj.name or False, - 'object_date': hasattr(obj, 'date') and obj.date or False, - 'object_description': hasattr(obj, 'description') and obj.description or False, - 'object_user': hasattr(obj, 'user_id') and (obj.user_id and obj.user_id.name) or '/', + 'object_id': obj.id, + 'object_subject': hasattr(obj, 'name') and obj.name or False, + 'object_date': hasattr(obj, 'date') and obj.date or False, + 'object_description': hasattr(obj, 'description') and obj.description or False, + 'object_user': hasattr(obj, 'user_id') and (obj.user_id and obj.user_id.name) or '/', 'object_user_email': hasattr(obj, 'user_id') and (obj.user_id and \ - obj.user_id.address_id and obj.user_id.address_id.email) or '/', + obj.user_id.address_id and obj.user_id.address_id.email) or '/', 'object_user_phone': hasattr(obj, 'user_id') and (obj.user_id and\ - obj.user_id.address_id and obj.user_id.address_id.phone) or '/', - 'partner': hasattr(obj, 'partner_id') and (obj.partner_id and obj.partner_id.name) or '/', + obj.user_id.address_id and obj.user_id.address_id.phone) or '/', + 'partner': hasattr(obj, 'partner_id') and (obj.partner_id and obj.partner_id.name) or '/', 'partner_email': hasattr(obj, 'partner_address_id') and (obj.partner_address_id and\ - obj.partner_address_id.email) or '/', + obj.partner_address_id.email) or '/', } return self.format_body(body % data) - def email_send(self, cr, uid, obj, emails, body, emailfrom=tools.config.get('email_from',False), context={}): + def email_send(self, cr, uid, obj, emails, body, emailfrom=tools.config.get('email_from', False), context={}): """ send email @param self: The object pointer @param cr: the current row, from the database cursor, @@ -92,7 +177,7 @@ class base_action_rule(osv.osv): emailfrom = tools.ustr(emailfrom) reply_to = emailfrom if not emailfrom: - raise osv.except_osv(_('Error!'), + raise osv.except_osv(_('Error!'), _("No E-Mail ID Found for your Company address!")) return tools.email_send(emailfrom, emails, name, body, reply_to=reply_to, openobject_id=str(obj.id)) @@ -105,6 +190,10 @@ class base_action_rule(osv.osv): @param context: A standard dictionary for contextual values """ ok = True + if eval(action.domain): + obj_ids = obj._table.search(cr, uid, eval(action.domain), context=context) + if not obj.id in obj_ids: + ok = False if hasattr(obj, 'user_id'): ok = ok and (not action.trg_user_id.id or action.trg_user_id.id==obj.user_id.id) if hasattr(obj, 'partner_id'): @@ -122,10 +211,6 @@ class base_action_rule(osv.osv): if state_to: ok = ok and (not action.trg_state_to or action.trg_state_to==state_to) - if hasattr(obj, 'priority'): - ok = ok and (not action.trg_priority_from or action.trg_priority_from>=obj.priority) - ok = ok and (not action.trg_priority_to or action.trg_priority_to<=obj.priority) - reg_name = action.regex_name result_name = True if reg_name: @@ -147,7 +232,7 @@ class base_action_rule(osv.osv): @param context: A standard dictionary for contextual values """ if action.server_action_id: - context.update({'active_id':obj.id,'active_ids':[obj.id]}) + context.update({'active_id':obj.id, 'active_ids':[obj.id]}) self.pool.get('ir.actions.server').run(cr, uid, [action.server_action_id.id], context) write = {} if hasattr(obj, 'user_id') and action.act_user_id: @@ -163,10 +248,6 @@ class base_action_rule(osv.osv): obj.categ_id = action.act_categ_id write['categ_id'] = action.act_categ_id.id - if hasattr(obj, 'priority') and action.act_priority: - obj.priority = action.act_priority - write['priority'] = action.act_priority - model_obj.write(cr, uid, [obj.id], write, context) if hasattr(model_obj, 'remind_user') and action.act_remind_user: @@ -201,175 +282,58 @@ class base_action_rule(osv.osv): if not scrit: scrit = [] - rule_line_obj = self.pool.get('base.action.rule.line') - for rule in self.browse(cr, uid, ids): - level = rule.max_level + cr.execute("select id from base_action_rule order by sequence") + rule_ids = map(lambda x: x[0], cr.fetchall()) + for action in self.browse(cr, uid, rule_ids): + level = action.max_level if not level: break - newactions = [] - scrit += [('rule_id','=',rule.id)] - line_ids = rule_line_obj.search(cr, uid, scrit) - actions = rule_line_obj.browse(cr, uid, line_ids, context=context) - model_obj = self.pool.get(rule.name.model) + model_obj = self.pool.get(action.name.model) for obj in objects: - for action in actions: - ok = self.do_check(cr, uid, action, obj, context=context) - if not ok: - continue + ok = self.do_check(cr, uid, action, obj, context=context) + if not ok: + continue - base = False - if hasattr(obj, 'create_date') and action.trg_date_type=='create': - base = mx.DateTime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S') - elif hasattr(obj, 'create_date') and action.trg_date_type=='action_last': - if hasattr(obj, 'date_action_last') and obj.date_action_last: - base = mx.DateTime.strptime(obj.date_action_last, '%Y-%m-%d %H:%M:%S') - else: - base = mx.DateTime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S') - elif hasattr(obj, 'date_deadline') and action.trg_date_type=='deadline' \ - and obj.date_deadline: - base = mx.DateTime.strptime(obj.date_deadline, '%Y-%m-%d %H:%M:%S') - elif hasattr(obj, 'date') and action.trg_date_type=='date' and obj.date: - base = mx.DateTime.strptime(obj.date, '%Y-%m-%d %H:%M:%S') - if base: - fnct = { - 'minutes': lambda interval: mx.DateTime.RelativeDateTime(minutes=interval), - 'day': lambda interval: mx.DateTime.RelativeDateTime(days=interval), - 'hour': lambda interval: mx.DateTime.RelativeDateTime(hours=interval), - 'month': lambda interval: mx.DateTime.RelativeDateTime(months=interval), - } - d = base + fnct[action.trg_date_range_type](action.trg_date_range) - dt = d.strftime('%Y-%m-%d %H:%M:%S') - ok = False - if hasattr(obj, 'date_action_last') and hasattr(obj, 'date_action_next'): - ok = (dt <= time.strftime('%Y-%m-%d %H:%M:%S')) and \ - ((not obj.date_action_next) or \ - (dt >= obj.date_action_next and \ - obj.date_action_last < obj.date_action_next)) - if not ok: - if not obj.date_action_next or dt < obj.date_action_next: - obj.date_action_next = dt - model_obj.write(cr, uid, [obj.id], {'date_action_next': dt}, context) + base = False + if hasattr(obj, 'create_date') and action.trg_date_type=='create': + base = mx.DateTime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S') + elif hasattr(obj, 'create_date') and action.trg_date_type=='action_last': + if hasattr(obj, 'date_action_last') and obj.date_action_last: + base = mx.DateTime.strptime(obj.date_action_last, '%Y-%m-%d %H:%M:%S') else: - ok = action.trg_date_type=='none' + base = mx.DateTime.strptime(obj.create_date[:19], '%Y-%m-%d %H:%M:%S') + elif hasattr(obj, 'date_deadline') and action.trg_date_type=='deadline' \ + and obj.date_deadline: + base = mx.DateTime.strptime(obj.date_deadline, '%Y-%m-%d %H:%M:%S') + elif hasattr(obj, 'date') and action.trg_date_type=='date' and obj.date: + base = mx.DateTime.strptime(obj.date, '%Y-%m-%d %H:%M:%S') + if base: + fnct = { + 'minutes': lambda interval: mx.DateTime.RelativeDateTime(minutes=interval), + 'day': lambda interval: mx.DateTime.RelativeDateTime(days=interval), + 'hour': lambda interval: mx.DateTime.RelativeDateTime(hours=interval), + 'month': lambda interval: mx.DateTime.RelativeDateTime(months=interval), + } + d = base + fnct[action.trg_date_range_type](action.trg_date_range) + dt = d.strftime('%Y-%m-%d %H:%M:%S') + ok = False + if hasattr(obj, 'date_action_last') and hasattr(obj, 'date_action_next'): + ok = (dt <= time.strftime('%Y-%m-%d %H:%M:%S')) and \ + ((not obj.date_action_next) or \ + (dt >= obj.date_action_next and \ + obj.date_action_last < obj.date_action_next)) + if not ok: + if not obj.date_action_next or dt < obj.date_action_next: + obj.date_action_next = dt + model_obj.write(cr, uid, [obj.id], {'date_action_next': dt}, context) + else: + ok = action.trg_date_type=='none' - if ok: - self.do_action(cr, uid, action, model_obj, obj, context) - break + if ok: + self.do_action(cr, uid, action, model_obj, obj, context) + break level -= 1 return True -base_action_rule() - -class base_action_rule_line(osv.osv): - _name = 'base.action.rule.line' - _description = 'Action Rule Lines' - - def _state_get(self, cr, uid, context={}): - """ Get State - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values """ - - return self.state_get(cr, uid, context=context) - def _priority_get(self, cr, uid, context={}): - """ Get Priority - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values """ - - return self.priority_get(cr, uid, context=context) - - def state_get(self, cr, uid, context={}): - """ Get State - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values """ - - return [('','')] - def priority_get(self, cr, uid, context={}): - """ Get Priority - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values """ - - return [('','')] - - _columns = { - 'name': fields.char('Rule Name',size=64, required=True), - 'rule_id': fields.many2one('base.action.rule','Rule'), - 'active': fields.boolean('Active', help="If the active field is set to true,\ - it will allow you to hide the rule without removing it."), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when\ - displaying a list of rules."), - - 'trg_date_type': fields.selection([ - ('none','None'), - ('create','Creation Date'), - ('action_last','Last Action Date'), - ('date','Date'), - ('deadline', 'Deadline'), - ], 'Trigger Date', size=16), - 'trg_date_range': fields.integer('Delay after trigger date',help="Delay After Trigger Date,\ - specifies you can put a negative number " \ - "if you need a delay before the trigger date, like sending a reminder 15 minutes before a meeting."), - 'trg_date_range_type': fields.selection([('minutes', 'Minutes'),('hour','Hours'),\ - ('day','Days'),('month','Months')], 'Delay type'), - - - 'trg_user_id': fields.many2one('res.users', 'Responsible'), - - 'trg_partner_id': fields.many2one('res.partner', 'Partner'), - 'trg_partner_categ_id': fields.many2one('res.partner.category', 'Partner Category'), - 'trg_state_from': fields.selection(_state_get, 'State', size=16), - 'trg_state_to': fields.selection(_state_get, 'Button Pressed', size=16), - 'trg_priority_from': fields.selection(_priority_get, 'Minimum Priority'), - 'trg_priority_to': fields.selection(_priority_get, 'Maximum Priority'), - - 'act_method': fields.char('Call Object Method', size=64), - 'act_user_id': fields.many2one('res.users', 'Set responsible to'), - 'act_state': fields.selection(_state_get, 'Set state to', size=16), - 'act_priority': fields.selection(_priority_get, 'Set priority to'), - 'act_email_cc': fields.char('Add watchers (Cc)', size=250, help="These people\ - will receive a copy of the future communication between partner and users by email"), - - 'act_remind_partner': fields.boolean('Remind Partner', help="Check this if\ - you want the rule to send a reminder by email to the partner."), - 'act_remind_user': fields.boolean('Remind responsible', help="Check this if \ - you want the rule to send a reminder by email to the user."), - 'act_reply_to': fields.char('Reply-To', size=64), - 'act_remind_attach': fields.boolean('Remind with attachment', help="Check this if\ - you want that all documents attached to the object be attached \ - to the reminder email sent."), - - 'act_mail_to_user': fields.boolean('Mail to responsible',help="Check this if \ - you want the rule to send an email to the responsible person."), - 'act_mail_to_watchers': fields.boolean('Mail to watchers (CC)',help="Check this\ - if you want the rule to mark CC(mail to any other person\ - defined in actions)."), - 'act_mail_to_email': fields.char('Mail to these emails', size=128,help="Email-id \ - of the persons whom mail is to be sent"), - 'act_mail_body': fields.text('Mail body',help="Content of mail"), - 'regex_name': fields.char('Regular Expression on Model Name', size=128), - 'server_action_id': fields.many2one('ir.actions.server','Server Action',help="Describes the\ - action name." \ - "eg:on which object which action to be taken on basis of which condition"), - } - - _defaults = { - 'active': lambda *a: 1, - 'trg_date_type': lambda *a: 'none', - 'trg_date_range_type': lambda *a: 'day', - 'act_mail_to_user': lambda *a: 0, - 'act_remind_partner': lambda *a: 0, - 'act_remind_user': lambda *a: 0, - 'act_mail_to_watchers': lambda *a: 0, - } - - _order = 'sequence' - def _check_mail(self, cr, uid, ids, context=None): """ Check Mail @@ -390,9 +354,9 @@ class base_action_rule_line(osv.osv): return True _constraints = [ - (_check_mail, 'Error: The mail is not well formated', ['act_mail_body']), + (_check_mail, 'Error: The mail is not well formated', ['act_mail_body']), ] -base_action_rule_line() +base_action_rule() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/base_action_rule/base_action_rule_view.xml b/addons/base_action_rule/base_action_rule_view.xml index 09bbb2e2f5f..ce07647dc92 100644 --- a/addons/base_action_rule/base_action_rule_view.xml +++ b/addons/base_action_rule/base_action_rule_view.xml @@ -16,52 +16,9 @@
+ - - - - - - - - - - base.action.rule.tree - base.action.rule - tree - - - - - - - - - - - - - Action Rules - base.action.rule - form - tree,form - - - - - - - - base.action.rule.line.form - base.action.rule.line - form - -
- - + @@ -79,45 +36,46 @@ - - - - - - - - - - - - - - - - + - - - - - + + + + + + + + + + + + + + + + + + -
+ - + - - base.action.rule.line.tree - base.action.rule.line + + base.action.rule.tree + base.action.rule tree - - - + + + + + + + + + + + Action Rules + base.action.rule + form + tree,form + - + + diff --git a/addons/base_action_rule/security/ir.model.access.csv b/addons/base_action_rule/security/ir.model.access.csv index 982dfdebfb5..99e4fc07c4b 100644 --- a/addons/base_action_rule/security/ir.model.access.csv +++ b/addons/base_action_rule/security/ir.model.access.csv @@ -1,3 +1,2 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" "access_base_action_rule","base.action.rule","model_base_action_rule",,1,1,1,1 -"access_base_action_rule_line","base.action.rule.line","model_base_action_rule_line",,1,1,1,1 diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index f4f1e68a269..fd1d60914d1 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -19,7 +19,7 @@ # ############################################################################## -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from dateutil import parser from dateutil import rrule from osv import fields, osv @@ -36,7 +36,7 @@ months = { 10: "October", 11: "November", 12: "December" } -def get_recurrent_dates(rrulestring, exdate, startdate=None): +def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None): """ Get recurrent dates based on Rule string considering exdate and start date @param rrulestring: Rulestring @@ -56,6 +56,8 @@ def get_recurrent_dates(rrulestring, exdate, startdate=None): for date in exdate: datetime_obj = todate(date) rset1._exdate.append(datetime_obj) + if exrule: + rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate)) re_dates = map(lambda x:x.strftime('%Y-%m-%d %H:%M:%S'), rset1._iter()) return re_dates @@ -176,28 +178,12 @@ html_invitation = """ - - Are you coming? - : - -
    -
  • YES
  • -
  • NO
  • -
  • MAYBE
  • -
- - - - - @@ -257,22 +243,22 @@ class calendar_attendee(osv.osv): attdata.sent_by_uid.address_id.email) if name == 'cn': if attdata.user_id: - result[id][name] = self._get_address(attdata.user_id.name, attdata.email) + result[id][name] = attdata.user_id.name elif attdata.partner_address_id: - result[id][name] = self._get_address(attdata.partner_id.name, attdata.email) + result[id][name] = attdata.partner_address_id.name or attdata.partner_id.name else: - result[id][name] = self._get_address(None, attdata.email) + result[id][name] = attdata.email or '' if name == 'delegated_to': todata = [] - for parent in attdata.parent_ids: - if parent.email: - todata.append('MAILTO:' + parent.email) + for child in attdata.child_ids: + if child.email: + todata.append('MAILTO:' + child.email) result[id][name] = ', '.join(todata) if name == 'delegated_from': fromdata = [] - for child in attdata.child_ids: - if child.email: - fromdata.append('MAILTO:' + child.email) + for parent in attdata.parent_ids: + if parent.email: + fromdata.append('MAILTO:' + parent.email) result[id][name] = ', '.join(fromdata) if name == 'event_date': if attdata.ref: @@ -383,49 +369,72 @@ property or property parameter."), 'state': lambda *x: 'needs-action', } - response_re = re.compile("Are you coming\?.*\n*.*(YES|NO|MAYBE).*", re.UNICODE) - - def msg_new(self, cr, uid, msg): - """ + def get_ics_file(self, cr, uid, event_obj, context=None): + """ + Returns iCalendar file for the event invitation @param self: The object pointer @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - """ - return False - - def msg_act_get(self, msg): - """ - Get Message. - @param self: The object pointer - @return: dictionary of actions which contain state field value. - """ - - mailgate_obj = self.pool.get('mail.gateway') - body = mailgate_obj.msg_body_get(msg) - actions = {} - res = self.response_re.findall(body['body']) - if res: - actions['state'] = res[0] - return actions - - def msg_update(self, cr, uid, ids, msg, data={}, default_act='None'): - """ - Update msg state which may be accepted.declined.tentative. - @param cr: the current row, from the database cursor, @param uid: the current user’s ID for security checks, - @param ids: List of calendar attendee’s IDs. + @param event_obj: Event object (browse record) @param context: A standard dictionary for contextual values - @return: True + @return: .ics file content """ - msg_actions = self.msg_act_get(msg) - if msg_actions.get('state'): - if msg_actions['state'] in ['YES', 'NO', 'MAYBE']: - mapping = {'YES': 'accepted', 'NO': 'declined', 'MAYBE': 'tentative'} - status = mapping[msg_actions['state']] - print 'Got response for invitation id: %s as %s' % (ids, status) - self.write(cr, uid, ids, {'state': status}) - return True - + res = None + def ics_datetime(idate, short=False): + if short: + return date.fromtimestamp(time.mktime(time.strptime(idate, '%Y-%m-%d'))) + else: + return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S') + try: + import vobject + except ImportError: + return res + cal = vobject.iCalendar() + event = cal.add('vevent') + event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S')) + event.add('dtstart').value = ics_datetime(event_obj.date) + event.add('dtend').value = ics_datetime(event_obj.date_deadline) + event.add('summary').value = event_obj.name + if event_obj.description: + event.add('description').value = event_obj.description + if event_obj.location: + event.add('location').value = event_obj.location + if event_obj.rrule: + event.add('rrule').value = event_obj.rrule + + if event_obj.alarm_id: + # computes alarm data + valarm = event.add('valarm') + alarm_object = self.pool.get('res.alarm') + alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context) + # Compute trigger data + interval = alarm_data['trigger_interval'] + occurs = alarm_data['trigger_occurs'] + duration = (occurs == 'after' and alarm_data['trigger_duration']) \ + or -(alarm_data['trigger_duration']) + related = alarm_data['trigger_related'] + trigger = valarm.add('TRIGGER') + trigger.params['related'] = [related.upper()] + if interval == 'days': + delta = timedelta(days=duration) + if interval == 'hours': + delta = timedelta(hours=duration) + if interval == 'minutes': + delta = timedelta(minutes=duration) + trigger.value = delta + + # Compute other details + valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP' + + for attendee in event_obj.attendee_ids: + attendee_add = event.add('attendee') + attendee_add.params['CUTYPE'] = [str(attendee.cutype)] + attendee_add.params['ROLE'] = [str(attendee.role)] + attendee_add.params['RSVP'] = [str(attendee.rsvp)] + attendee_add.value = 'MAILTO:' + attendee.email + res = cal.serialize() + return res + def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context={}): """ Send mail for calendar attendee. @@ -443,7 +452,7 @@ property or property parameter."), res_obj = att.ref sub = '[%s Invitation][%d] %s' % (company, att.id, res_obj.name) att_infos = [] - other_invitaion_ids = self.search(cr, uid, [('ref', '=', att.ref._name + ',' + str(att.ref.id))]) + other_invitaion_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))]) for att2 in self.browse(cr, uid, other_invitaion_ids): att_infos.append(((att2.user_id and att2.user_id.name) or \ (att2.partner_id and att2.partner_id.name) or \ @@ -459,15 +468,17 @@ property or property parameter."), 'company': company } body = html_invitation % body_vals + attach = self.get_ics_file(cr, uid, res_obj, context=context) if mail_to and email_from: tools.email_send( - email_from, - mail_to, - sub, - body, - subtype='html', - reply_to=email_from - ) + email_from, + mail_to, + sub, + body, + attach=attach and [('invitation.ics', attach)] or None, + subtype='html', + reply_to=email_from + ) return True def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv): @@ -569,7 +580,7 @@ class res_alarm(osv.osv): are both optional, but if one occurs, so MUST the other"""), 'repeat': fields.integer('Repeat'), 'active': fields.boolean('Active', help="If the active field is set to \ - true, it will allow you to hide the event alarm information without removing it.") +true, it will allow you to hide the event alarm information without removing it.") } _defaults = { 'trigger_interval': lambda *x: 'minutes', @@ -659,41 +670,41 @@ class calendar_alarm(osv.osv): __attribute__ = {} _columns = { - 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'), - 'name': fields.char('Summary', size=124, help="""Contains the text to be \ - used as the message subject for email \ - or contains the text to be used for display"""), - 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \ - ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \ - required=True, help="Defines the action to be invoked when an alarm is triggered"), - 'description': fields.text('Description', help='Provides a more complete \ - description of the calendar component, than that \ - provided by the "SUMMARY" property'), - 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \ - 'alarm_id', 'attendee_id', 'Attendees', readonly=True), - 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\ - which is rendered when the alarm is triggered for audio, - * File which is intended to be sent as message attachments for email, - * Points to a procedure resource, which is invoked when\ - the alarm is triggered for procedure."""), - 'res_id': fields.integer('Resource ID'), - 'model_id': fields.many2one('ir.model', 'Model'), - 'user_id': fields.many2one('res.users', 'Owner'), - 'event_date': fields.datetime('Event Date'), - 'event_end_date': fields.datetime('Event End Date'), - 'trigger_date': fields.datetime('Trigger Date', readonly="True"), - 'state':fields.selection([('draft', 'Draft'), - ('run', 'Run'), - ('stop', 'Stop'), - ('done', 'Done'), - ], 'State', select=True, readonly=True), - } + 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'), + 'name': fields.char('Summary', size=124, help="""Contains the text to be \ + used as the message subject for email \ + or contains the text to be used for display"""), + 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \ + ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \ + required=True, help="Defines the action to be invoked when an alarm is triggered"), + 'description': fields.text('Description', help='Provides a more complete \ + description of the calendar component, than that \ + provided by the "SUMMARY" property'), + 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \ + 'alarm_id', 'attendee_id', 'Attendees', readonly=True), + 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\ + which is rendered when the alarm is triggered for audio, + * File which is intended to be sent as message attachments for email, + * Points to a procedure resource, which is invoked when\ + the alarm is triggered for procedure."""), + 'res_id': fields.integer('Resource ID'), + 'model_id': fields.many2one('ir.model', 'Model'), + 'user_id': fields.many2one('res.users', 'Owner'), + 'event_date': fields.datetime('Event Date'), + 'event_end_date': fields.datetime('Event End Date'), + 'trigger_date': fields.datetime('Trigger Date', readonly="True"), + 'state':fields.selection([ + ('draft', 'Draft'), + ('run', 'Run'), + ('stop', 'Stop'), + ('done', 'Done'), + ], 'State', select=True, readonly=True), + } _defaults = { - 'action': lambda *x: 'email', - 'state': lambda *x: 'run', - } - + 'action': lambda *x: 'email', + 'state': lambda *x: 'run', + } def create(self, cr, uid, vals, context={}): """ create new record. @@ -983,7 +994,7 @@ class calendar_event(osv.osv): 'exdate': fields.text('Exception Date/Times', help="This property \ defines the list of date/time exceptions for arecurring calendar component."), 'exrule': fields.char('Exception Rule', size=352, help="defines a \ - rule or repeating pattern for anexception to a recurrence set"), +rule or repeating pattern for an exception to a recurrence set"), 'rrule': fields.function(_get_rulestring, type='char', size=124, method=True, \ string='Recurrent Rule', store=True, \ fnct_inv=_set_rrulestring, help='Defines a\ @@ -1031,7 +1042,9 @@ e.g.: Every other month on the last Sunday of the month for 10 occurrences:\ 'end_date': fields.date('Repeat Until'), 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel',\ 'event_id', 'attendee_id', 'Attendees'), - 'allday': fields.boolean('All Day') + 'allday': fields.boolean('All Day'), + 'active': fields.boolean('Active', help="If the active field is set to \ +true, it will allow you to hide the event alarm information without removing it.") } _defaults = { @@ -1040,6 +1053,7 @@ e.g.: Every other month on the last Sunday of the month for 10 occurrences:\ 'freq': lambda *x: 'None', 'select1': lambda *x: 'date', 'interval': lambda *x: 1, + 'active': lambda *x: 1, } def open_event(self, cr, uid, ids, context=None): @@ -1164,8 +1178,10 @@ e.g.: Every other month on the last Sunday of the month for 10 occurrences:\ if count > limit: break event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S") - if start_date and start_date <= event_date: - start_date = event_date +# To check: If the start date is replace by event date .. the event date will be changed by that of calendar code +# if start_date and start_date <= event_date: +# start_date = event_date + start_date = event_date if not data['rrule']: if start_date and (event_date < start_date): continue @@ -1213,7 +1229,7 @@ e.g.: Every other month on the last Sunday of the month for 10 occurrences:\ if isinstance(select, (str, int, long)): return ids and ids[0] or False return ids - + def compute_rule_string(self, cr, uid, datas, context=None, *args): """ Compute rule string. @@ -1332,6 +1348,7 @@ e.g.: Every other month on the last Sunday of the month for 10 occurrences:\ event_id = base_calendar_id2real_id(event_id) if not event_id in new_ids: new_ids.append(event_id) + 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'): alarm_obj = self.pool.get('res.alarm') diff --git a/addons/base_calendar/base_calendar_view.xml b/addons/base_calendar/base_calendar_view.xml index 2a48c0be32f..0c8a52260ef 100644 --- a/addons/base_calendar/base_calendar_view.xml +++ b/addons/base_calendar/base_calendar_view.xml @@ -8,8 +8,13 @@ form
- - + + + + + + + @@ -20,9 +25,6 @@ string="Partner Contact" /> - - - @@ -181,7 +183,7 @@ calendar.event form - + @@ -190,7 +192,7 @@ + on_change="onchange_dates(date,duration,False,allday)" attrs="{'invisible': [('allday', '=', True)]}"/> @@ -257,7 +259,6 @@ - @@ -333,6 +334,7 @@ + @@ -353,6 +355,44 @@ + + + + Calendar Events Search + calendar.event + search + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -361,6 +401,7 @@ calendar.event form tree,form,calendar + diff --git a/addons/base_calendar/wizard/base_calendar_invite_attendee.py b/addons/base_calendar/wizard/base_calendar_invite_attendee.py index 0e91447eebf..cb3e4d2e62a 100644 --- a/addons/base_calendar/wizard/base_calendar_invite_attendee.py +++ b/addons/base_calendar/wizard/base_calendar_invite_attendee.py @@ -44,7 +44,7 @@ class base_calendar_invite_attendee(osv.osv_memory): 'contact_ids': fields.many2many('res.partner.address', 'invite_contact_rel', 'invite_id', 'contact_id', 'Contacts'), 'send_mail': fields.boolean('Send mail?', help='Check this if you want to \ - send an Email to Invited Person') +send an Email to Invited Person') } _defaults = { @@ -66,20 +66,19 @@ class base_calendar_invite_attendee(osv.osv_memory): model = False model_field = False - context_id = context and context.get('active_id', False) or False if not context or not context.get('model'): return {} else: model = context.get('model') + model_field = context.get('attendee_field', False) + obj = self.pool.get(model) + res_obj = obj.browse(cr, uid, context_id) + att_obj = self.pool.get('calendar.attendee') for datas in self.read(cr, uid, ids, context=context): - - obj = self.pool.get(model) - res_obj = obj.browse(cr, uid, context_id) type = datas.get('type') - att_obj = self.pool.get('calendar.attendee') vals = [] mail_to = [] attendees = [] @@ -123,15 +122,13 @@ class base_calendar_invite_attendee(osv.osv_memory): if contact.email: mail_to.append(contact.email) - att = att_obj.browse(cr, uid, context_id) - for att_val in vals: if model == 'calendar.attendee': - if ref: - att_val.update({ - 'parent_ids': [(4, att.id)], - 'ref': att.ref._name + ',' +str(att.ref.id) - }) + att = att_obj.browse(cr, uid, context_id) + att_val.update({ + 'parent_ids': [(4, att.id)], + 'ref': att.ref._name + ',' +str(att.ref.id) + }) attendees.append(att_obj.create(cr, uid, att_val)) if model_field: for attendee in attendees: diff --git a/addons/base_setup/installer.py b/addons/base_setup/installer.py index a92d5ab4b97..b7123b1ba38 100644 --- a/addons/base_setup/installer.py +++ b/addons/base_setup/installer.py @@ -73,6 +73,7 @@ class base_setup_installer(osv.osv_memory): 'report_designer':fields.boolean('Advanced Reporting', help="Lets you install various tools to simplify and enhance " "OpenERP's report creation."), + 'thunderbird' :fields.boolean('Thunderbird'), # Vertical modules 'profile_association':fields.boolean('Associations', help="Installs a preselected set of OpenERP " diff --git a/addons/board_crm/board_crm_statistical_view.xml b/addons/board_crm/board_crm_statistical_view.xml index 677dd3d0138..ba952a87e70 100644 --- a/addons/board_crm/board_crm_statistical_view.xml +++ b/addons/board_crm/board_crm_statistical_view.xml @@ -18,7 +18,7 @@ report.crm.opportunity.categ.graph - crm.opportunity.report + crm.lead.report graph @@ -30,7 +30,7 @@ - - - CRM Configuration - - - crm.opportunity.user.graph1 - crm.opportunity.report - graph - - - - - - - - - crm.lead - form - tree,form - + + + CRM Configuration + + + crm.opportunity.user.graph1 + crm.lead.report + graph + + + + + + + + + crm.lead + form + tree,form + [('user_id','=',uid),('state','!=','pending'),('state','!=','cancel')] - - - crm.meeting - form - tree,form - + + + crm.meeting + form + tree,form + + [('user_id','=',uid),('state','!=','pending'),('state','!=','cancel')] + + + + crm.lead.report + form + graph,tree,form + [('user_id','=',uid),('state','!=','pending'),('state','!=','cancel')] - - - - crm.lead.report - form - graph,tree,form - - [('user_id','=',uid),('state','!=','pending'),('state','!=','cancel')] - + - - crm.opportunity.report - form - graph,tree,form - - [('user_id','=',uid),('state','!=','pending'),('state','!=','cancel')] - + + crm.lead.report + form + graph,tree,form + + [('user_id','=',uid),('state','!=','pending'),('state','!=','cancel'),('type', '=', 'opportunity')] + - - CRM - Dashboard Form - board.board - form - - - + + CRM - Dashboard Form + board.board + form + + + - + - + - + - + - - - - + diff --git a/addons/caldav/DAV/BufferingHTTPServer.py b/addons/caldav/DAV/BufferingHTTPServer.py deleted file mode 100644 index bf2e9e59800..00000000000 --- a/addons/caldav/DAV/BufferingHTTPServer.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python - -""" - Buffering HTTP Server - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" - - -from utils import VERSION, AUTHOR -__version__ = VERSION -__author__ = AUTHOR - -from BaseHTTPServer import BaseHTTPRequestHandler -import os - -class BufferedHTTPRequestHandler(BaseHTTPRequestHandler): - """ - Buffering HTTP Request Handler - - This class is an extension to the BaseHTTPRequestHandler - class which buffers the whole output and sends it at once - after the processing if the request is finished. - - This makes it possible to work together with some clients - which otherwise would break (e.g. cadaver) - - """ - - - def _init_buffer(self): - """initialize the buffer. - - If you override the handle() method remember to call - this (see below) - """ - self.__buffer = "" - self.__outfp = os.tmpfile() - - def _append(self,s): - """ append a string to the buffer """ - self.__buffer = self.__buffer+s - - def _flush(self): - """ flush the buffer to wfile """ - self.wfile.write(self.__buffer) - self.__outfp.write(self.__buffer) - self.__outfp.flush() - self.wfile.flush() - self.__buffer = "" - - def handle(self): - """ Handle a HTTP request """ - self._init_buffer() - BaseHTTPRequestHandler.handle(self) - self._flush() - - def send_header(self, keyword, value): - """Send a MIME header.""" - if self.request_version != 'HTTP/0.9': - self._append("%s: %s\r\n" % (keyword, value)) - - def end_headers(self): - """Send the blank line ending the MIME headers.""" - if self.request_version != 'HTTP/0.9': - self._append("\r\n") - - def send_response(self, code, message=None): - self.log_request(code) - - if message is None: - if self.responses.has_key(code): - message = self.responses[code][0] - else: - message = '' - - if self.request_version != 'HTTP/0.9': - self._append("%s %s %s\r\n" % - (self.protocol_version, str(code), message)) - - self.send_header('Server', self.version_string()) - self.send_header('Connection', 'close') - self.send_header('Date', self.date_time_string()) - - protocol_version="HTTP/1.1" - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/WebDAVServer.py b/addons/caldav/DAV/WebDAVServer.py deleted file mode 100644 index fa30aa6b82b..00000000000 --- a/addons/caldav/DAV/WebDAVServer.py +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/env python - -""" - Python WebDAV Server. - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -This module builds on AuthServer by implementing the standard DAV -methods. - -Subclass this class and specify an IFACE_CLASS. See example. - -""" - -DEBUG = None - -from utils import VERSION, AUTHOR -__version__ = VERSION -__author__ = AUTHOR - -from propfind import PROPFIND -from delete import DELETE -from davcopy import COPY -from davmove import MOVE - -from string import atoi, split -from status import STATUS_CODES -from errors import * - -import os -import sys -import time -import socket -import string -import posixpath -import base64 -import urlparse -import urllib -import BaseHTTPServer - -class DAVRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """Simple DAV request handler with - - - GET - - HEAD - - PUT - - OPTIONS - - PROPFIND - - PROPPATCH - - MKCOL - - It uses the resource/collection classes for serving and - storing content. - - """ - - server_version = "DAV/" + __version__ - protocol_version = 'HTTP/1.1' - - ### utility functions - def _log(self, message): - pass - - def _append(self, s): - """ write the string to wfile """ - self.wfile.write(s) - - def send_body(self, DATA, code, msg, desc, ctype='application/octet-stream', headers=None): - """ send a body in one part """ - - if not headers: - headers = {} - self.send_response(code, message=msg) - self.send_header("Connection", "keep-alive") - self.send_header("Accept-Ranges", "bytes") - - for a, v in headers.items(): - self.send_header(a, v) - - if DATA: - self.send_header("Content-Length", str(len(DATA))) - self.send_header("Content-Type", ctype) - else: - self.send_header("Content-Length", "0") - - self.end_headers() - if DATA: - self._append(DATA) - - def send_body_chunks(self, DATA, code, msg, desc, ctype='text/xml; encoding="utf-8"'): - """ send a body in chunks """ - - self.responses[207]=(msg, desc) - self.send_response(code, message=msg) - self.send_header("Content-type", ctype) - self.send_header("Connection", "keep-alive") - self.send_header("Transfer-Encoding", "chunked") - self.end_headers() - self._append(hex(len(DATA))[2:]+"\r\n") - self._append(DATA) - self._append("\r\n") - self._append("0\r\n") - self._append("\r\n") - - ### HTTP METHODS - - def do_OPTIONS(self): - """return the list of capabilities """ - self.send_response(200) - self.send_header("Allow", "GET, HEAD, COPY, MOVE, POST, PUT, PROPFIND, PROPPATCH, OPTIONS, MKCOL, DELETE, TRACE") - self.send_header("Content-Type", "text/plain") - self.send_header("Connection", "keep-alive") - self.send_header("DAV", "1") - self.end_headers() - - def do_PROPFIND(self): - - dc = self.IFACE_CLASS - # read the body - body = None - if self.headers.has_key("Content-Length"): - l = self.headers['Content-Length'] - body = self.rfile.read(atoi(l)) - alt_body = """ - - - - - - - - - - """ - #self.wfile.write(body) - - # which Depth? - if self.headers.has_key('Depth'): - d = self.headers['Depth'] - else: - d = "infinity" - - uri = self.geturi() - pf = PROPFIND(uri, dc, d) - - if body: - pf.read_propfind(body) - - try: - DATA = pf.createResponse() - DATA = DATA+"\n" - # print "Data:", DATA - except DAV_NotFound, (ec, dd): - return self.send_notFound(dd, uri) - except DAV_Error, (ec, dd): - return self.send_error(ec, dd) - - self.send_body_chunks(DATA, 207, "Multi-Status", "Multiple responses") - - def geturi(self): - buri = self.IFACE_CLASS.baseuri - if buri[-1] == '/': - return urllib.unquote(buri[:-1]+self.path) - else: - return urllib.unquote(buri+self.path) - - def do_GET(self): - """Serve a GET request.""" - dc = self.IFACE_CLASS - uri = self.geturi() - - # get the last modified date - try: - lm = dc.get_prop(uri, "DAV:", "getlastmodified") - except: - lm = "Sun, 01 Dec 2014 00:00:00 GMT" # dummy! - headers = {"Last-Modified":lm , "Connection": "keep-alive"} - - # get the content type - try: - ct = dc.get_prop(uri, "DAV:", "getcontenttype") - except: - ct = "application/octet-stream" - - # get the data - try: - data = dc.get_data(uri) - except DAV_Error, (ec, dd): - self.send_status(ec) - return - - # send the data - self.send_body(data, 200, "OK", "OK", ct, headers) - - def do_HEAD(self): - """ Send a HEAD response """ - dc = self.IFACE_CLASS - uri = self.geturi() - - # get the last modified date - try: - lm = dc.get_prop(uri, "DAV:", "getlastmodified") - except: - lm = "Sun, 01 Dec 2014 00:00:00 GMT" # dummy! - - headers = {"Last-Modified":lm, "Connection": "keep-alive"} - - # get the content type - try: - ct = dc.get_prop(uri, "DAV:", "getcontenttype") - except: - ct = "application/octet-stream" - - try: - data = dc.get_data(uri) - headers["Content-Length"] = str(len(data)) - except DAV_NotFound: - self.send_body(None, 404, "Not Found", "") - return - - self.send_body(None, 200, "OK", "OK", ct, headers) - - def do_POST(self): - self.send_error(404, "File not found") - - def do_MKCOL(self): - """ create a new collection """ - - dc = self.IFACE_CLASS - uri = self.geturi() - try: - res = dc.mkcol(uri) - if res: - self.send_body(None, 201, "Created", '') - else: - self.send_body(None, 415, "Cannot create", '') - #self.send_header("Connection", "keep-alive") - # Todo: some content, too - except DAV_Error, (ec, dd): - self.send_body(None, int(ec), dd, dd) - - def do_DELETE(self): - """ delete an resource """ - dc = self.IFACE_CLASS - uri = self.geturi() - dl = DELETE(uri, dc) - if dc.is_collection(uri): - res = dl.delcol() - else: - res = dl.delone() - - if res: - self.send_status(207, body=res) - else: - self.send_status(204) - - def do_PUT(self): - dc = self.IFACE_CLASS - - # read the body - body = None - if self.headers.has_key("Content-Length"): - l = self.headers['Content-Length'] - body = self.rfile.read(atoi(l)) - uri = self.geturi() - - ct = None - if self.headers.has_key("Content-Type"): - ct = self.headers['Content-Type'] - try: - dc.put(uri, body, ct) - except DAV_Error, (ec, dd): - self.send_status(ec) - return - self.send_status(201) - - def do_COPY(self): - """ copy one resource to another """ - try: - self.copymove(COPY) - except DAV_Error, (ec, dd): - self.send_status(ec) - - def do_MOVE(self): - """ move one resource to another """ - try: - self.copymove(MOVE) - except DAV_Error, (ec, dd): - self.send_status(ec) - - def copymove(self, CLASS): - """ common method for copying or moving objects """ - dc = self.IFACE_CLASS - - # get the source URI - source_uri = self.geturi() - - # get the destination URI - dest_uri = self.headers['Destination'] - dest_uri = urllib.unquote(dest_uri) - - # Overwrite? - overwrite = 1 - result_code = 204 - if self.headers.has_key("Overwrite"): - if self.headers['Overwrite']=="F": - overwrite=None - result_code=201 - - # instanciate ACTION class - cp = CLASS(dc, source_uri, dest_uri, overwrite) - - # Depth? - d = "infinity" - if self.headers.has_key("Depth"): - d = self.headers['Depth'] - - if d!="0" and d!="infinity": - self.send_status(400) - return - - if d=="0": - res = cp.single_action() - self.send_status(res) - return - - # now it only can be "infinity" but we nevertheless check for a collection - if dc.is_collection(source_uri): - try: - res = cp.tree_action() - except DAV_Error, (ec, dd): - self.send_status(ec) - return - else: - try: - res = cp.single_action() - except DAV_Error, (ec, dd): - self.send_status(ec) - return - - if res: - self.send_body_chunks(res, 207, STATUS_CODES[207], STATUS_CODES[207], - ctype='text/xml; charset="utf-8"') - else: - self.send_status(result_code) - - def get_userinfo(self, user, pw): - """ Dummy method which lets all users in """ - - return 1 - - def send_status(self, code=200, mediatype='text/xml; charset="utf-8"', \ - msg=None, body=None): - - if not msg: msg = STATUS_CODES[code] - self.send_body(body, code, STATUS_CODES[code], msg, mediatype) - - def send_notFound(self, descr, uri): - body = """ - - %s - - %s - - """ - return self.send_status(404, descr, body=body % (uri, descr)) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/caldav/DAV/__init__.py b/addons/caldav/DAV/__init__.py deleted file mode 100644 index 10abdb40014..00000000000 --- a/addons/caldav/DAV/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" - python davserver - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -""" - - diff --git a/addons/caldav/DAV/constants.py b/addons/caldav/DAV/constants.py deleted file mode 100644 index e858ac519c2..00000000000 --- a/addons/caldav/DAV/constants.py +++ /dev/null @@ -1,18 +0,0 @@ -""" - -constants definition - - -""" - -# definition for resourcetype -COLLECTION=1 -OBJECT=None -DAV_PROPS=['creationdate', 'displayname', 'getcontentlanguage', 'getcontentlength', 'getcontenttype', 'getetag', 'getlastmodified', 'lockdiscovery', 'resourcetype', 'source', 'supportedlock'] - - -# Request classes in propfind - -RT_ALLPROP=1 -RT_PROPNAME=2 -RT_PROP=3 diff --git a/addons/caldav/DAV/davcmd.py b/addons/caldav/DAV/davcmd.py deleted file mode 100644 index 34c061d77ab..00000000000 --- a/addons/caldav/DAV/davcmd.py +++ /dev/null @@ -1,219 +0,0 @@ -""" - -davcmd.py ---------- - -containts commands like copy, move, delete for normal -resources and collections - -""" - -from string import split,replace,joinfields -import urlparse - -from utils import create_treelist, is_prefix -from errors import * - -def deltree(dc, uri, exclude={}): - """ delete a tree of resources - - dc -- dataclass to use - uri -- root uri to delete - exclude -- an optional list of uri:error_code pairs which should not - be deleted. - - returns dict of uri:error_code tuples from which - another method can create a multistatus xml element. - - Also note that we only know Depth=infinity thus we don't have - to test for it. - - """ - - tlist = create_treelist(dc,uri) - result = {} - - for i in range(len(tlist),0,-1): - problem_uris = result.keys() - element = tlist[i-1] - - # test here, if an element is a prefix of an uri which - # generated an error before. - # note that we walk here from childs to parents, thus - # we cannot delete a parent if a child made a problem. - # (see example in 8.6.2.1) - ok = 1 - for p in problem_uris: - if is_prefix(element,p): - ok = None - break - - if not ok: continue - - # here we test for the exclude list which is the other way round! - for p in exclude.keys(): - if is_prefix(p,element): - ok = None - break - - if not ok: continue - - # now delete stuff - try: - delone(dc,element) - except DAV_Error, (ec,dd): - result[element] = ec - - return result - -def delone(dc, uri): - """ delete a single object """ - if dc.is_collection(uri): - dc.rmcol(uri) # should be empty - else: - dc.rm(uri) - -### -### COPY -### - -# helper function - -def copy(dc, src, dst): - """ only copy the element - - This is just a helper method factored out from copy and - copytree. It will not handle the overwrite or depth header. - - """ - - # destination should have been deleted before - if dc.exists(dst): raise DAV_Error, 412 - - # source should exist also - if not dc.exists(src): raise DAV_NotFound - - if dc.is_collection(src): - dc.copycol(src,dst) # an exception will be passed thru - else: - dc.copy(src,dst) # an exception will be passed thru - - -# the main functions - -def copyone(dc, src, dst, overwrite=None): - """ copy one resource to a new destination """ - - if overwrite and dc.exists(dst): - delres = deltree(dc,dst) - else: - delres = {} - - # if we cannot delete everything, then do not copy! - if delres: return delres - - try: - copy(dc,src,dst) # pass thru exceptions - except DAV_Error, (ec,dd): - return ec - -def copytree(dc, src, dst, overwrite=None): - """ copy a tree of resources to another location - - dc -- dataclass to use - src -- src uri from where to copy - dst -- dst uri - overwrite -- if 1 then delete dst uri before - - returns dict of uri:error_code tuples from which - another method can create a multistatus xml element. - - """ - - - # first delete the destination resource - if overwrite and dc.exists(dst): - delres = deltree(dc,dst) - else: - delres = {} - - # if we cannot delete everything, then do not copy! - if delres: return delres - - # get the tree we have to copy - tlist = create_treelist(dc,src) - result = {} - - # prepare destination URIs (get the prefix) - dpath = urlparse.urlparse(dst)[2] - - for element in tlist: - problem_uris = result.keys() - - # now URIs get longer and longer thus we have - # to test if we had a parent URI which we were not - # able to copy in problem_uris which is the prefix - # of the actual element. If it is, then we cannot - # copy this as well but do not generate another error. - ok = 1 - for p in problem_uris: - if is_prefix(p,element): - ok = None - break - - if not ok: continue - - # now create the destination URI which corresponds to - # the actual source URI. -> actual_dst - # ("subtract" the base src from the URI and prepend the - # dst prefix to it.) - esrc = replace(element,src,"") - actual_dst = dpath+esrc - - # now copy stuff - try: - copy(dc,element,actual_dst) - except DAV_Error, (ec,dd): - result[element] = ec - - return result - - - -### -### MOVE -### - - -def moveone(dc, src, dst, overwrite=None): - """ move a single resource - - This is done by first copying it and then deleting - the original. - """ - - # first copy it - copyone(dc,src,dst,overwrite) - - # then delete it - dc.rm(src) - -def movetree(dc, src, dst, overwrite=None): - """ move a collection - - This is done by first copying it and then deleting - the original. - - PROBLEM: if something did not copy then we have a problem - when deleting as the original might get deleted! - """ - - # first copy it - res = copytree(dc,src,dst,overwrite) - - # then delete it - res = deltree(dc,src,exclude=res) - - return res - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/davcopy.py b/addons/caldav/DAV/davcopy.py deleted file mode 100644 index 6333aececf6..00000000000 --- a/addons/caldav/DAV/davcopy.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python - -""" - python davserver - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -""" - - -from xml.dom import ext -from xml.dom.Document import Document - -import sys -import string -import urlparse -import urllib -from StringIO import StringIO - -import utils -from constants import COLLECTION, OBJECT, DAV_PROPS, RT_ALLPROP, RT_PROPNAME, RT_PROP -from errors import * -from utils import create_treelist, quote_uri, gen_estring - -class COPY: - """ copy resources and eventually create multistatus responses - - This module implements the COPY class which is responsible for - copying resources. Usually the normal copy work is done in the - interface class. This class only creates error messages if error - occur. - - """ - - - def __init__(self, dataclass, src_uri, dst_uri, overwrite): - self.__dataclass = dataclass - self.__src = src_uri - self.__dst = dst_uri - self.__overwrite = overwrite - - - def single_action(self): - """ copy a normal resources. - - We try to copy it and return the result code. - This is for Depth==0 - - """ - - dc = self.__dataclass - base = self.__src - - ### some basic tests - # test if dest exists and overwrite is false - if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 - # test if src and dst are the same - # (we assume that both uris are on the same server!) - ps = urlparse.urlparse(self.__src)[2] - pd = urlparse.urlparse(self.__dst)[2] - if ps==pd: raise DAV_Error, 403 - - return dc.copyone(self.__src,self.__dst,self.__overwrite) - - #return copyone(dc,self.__src,self.__dst,self.__overwrite) - - def tree_action(self): - """ copy a tree of resources (a collection) - - Here we return a multistatus xml element. - - """ - dc = self.__dataclass - base = self.__src - - ### some basic tests - # test if dest exists and overwrite is false - if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 - # test if src and dst are the same - # (we assume that both uris are on the same server!) - ps = urlparse.urlparse(self.__src)[2] - pd = urlparse.urlparse(self.__dst)[2] - if ps==pd: raise DAV_Error, 403 - - - result = dc.copytree(self.__src,self.__dst,self.__overwrite) - #result=copytree(dc,self.__src,self.__dst,self.__overwrite) - - if not result: return None - - ### - ### create the multistatus XML element - ### (this is also the same as in delete.py. - ### we might make a common method out of it) - ### - - doc = Document(None) - ms = doc.createElement("D:multistatus") - ms.setAttribute("xmlns:D","DAV:") - doc.appendChild(ms) - - for el,ec in result.items(): - re = doc.createElement("D:response") - hr = doc.createElement("D:href") - st = doc.createElement("D:status") - huri = doc.createTextNode(quote_uri(el)) - t = doc.createTextNode(gen_estring(ec)) - st.appendChild(t) - hr.appendChild(huri) - re.appendChild(hr) - re.appendChild(st) - ms.appendChild(re) - - sfile = StringIO() - ext.PrettyPrint(doc,stream = sfile) - s = sfile.getvalue() - sfile.close() - return s - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/davmove.py b/addons/caldav/DAV/davmove.py deleted file mode 100644 index 3404f947469..00000000000 --- a/addons/caldav/DAV/davmove.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python - -""" - python davserver - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -""" - - -import sys -import string -import urlparse -import urllib -from StringIO import StringIO - -import utils -from constants import COLLECTION, OBJECT, DAV_PROPS -from constants import RT_ALLPROP, RT_PROPNAME, RT_PROP -from errors import * -from utils import create_treelist, quote_uri, gen_estring, make_xmlresponse -from davcmd import moveone, movetree - -class MOVE: - """ move resources and eventually create multistatus responses - - This module implements the MOVE class which is responsible for - moving resources. - - MOVE is implemented by a COPY followed by a DELETE of the old - resource. - - """ - - - def __init__(self, dataclass, src_uri, dst_uri, overwrite): - self.__dataclass = dataclass - self.__src = src_uri - self.__dst = dst_uri - self.__overwrite = overwrite - - - def single_action(self): - """ move a normal resources. - - We try to move it and return the result code. - This is for Depth==0 - - """ - - dc = self.__dataclass - base = self.__src - - ### some basic tests - # test if dest exists and overwrite is false - if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 - # test if src and dst are the same - # (we assume that both uris are on the same server!) - ps = urlparse.urlparse(self.__src)[2] - pd = urlparse.urlparse(self.__dst)[2] - if ps==pd: raise DAV_Error, 403 - - return dc.moveone(self.__src,self.__dst,self.__overwrite) - - def tree_action(self): - """ move a tree of resources (a collection) - - Here we return a multistatus xml element. - - """ - dc = self.__dataclass - base = self.__src - - ### some basic tests - # test if dest exists and overwrite is false - if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 - # test if src and dst are the same - # (we assume that both uris are on the same server!) - ps = urlparse.urlparse(self.__src)[2] - pd = urlparse.urlparse(self.__dst)[2] - if ps==pd: raise DAV_Error, 403 - - result = dc.movetree(self.__src,self.__dst,self.__overwrite) - if not result: return None - - # create the multistatus XML element - return make_xmlresponse(result) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/delete.py b/addons/caldav/DAV/delete.py deleted file mode 100644 index 3e75066f30e..00000000000 --- a/addons/caldav/DAV/delete.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -""" - - python davserver - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -""" -import os -import string -import urllib -from StringIO import StringIO - -from status import STATUS_CODES -from utils import gen_estring, quote_uri, make_xmlresponse -from davcmd import deltree - -class DELETE: - - def __init__(self, uri, dataclass): - self.__dataclass = dataclass - self.__uri = uri - - def delcol(self): - """ delete a collection """ - - dc = self.__dataclass - result = dc.deltree(self.__uri) - - if not len(result.items()): - return None # everything ok - - # create the result element - return make_xmlresponse(result) - - def delone(self): - """ delete a resource """ - - dc = self.__dataclass - result = dc.delone(self.__uri) - - if not result: return None - if not len(result.items()): - return None # everything ok - - # create the result element - return make_xmlresponse(result) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/errors.py b/addons/caldav/DAV/errors.py deleted file mode 100644 index 58d1d3be1d2..00000000000 --- a/addons/caldav/DAV/errors.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python -""" - - Exceptions for the DAVserver implementation - -""" - -class DAV_Error(Exception): - """ in general we can have the following arguments: - - 1. the error code - 2. the error result element, e.g. a element - """ - - def __init__(self, *args): - if len(args)==1: - self.args = (args[0],"") - else: - self.args = args - -class DAV_Secret(DAV_Error): - """ the user is not allowed to know anything about it - - returning this for a property value means to exclude it - from the response xml element. - """ - - def __init__(self): - DAV_Error.__init__(self,0) - pass - -class DAV_NotFound(DAV_Error): - """ a requested property was not found for a resource """ - - def __init__(self, *args): - if len(args): - if isinstance(args[0], list): - stre = "Path %s not found!"%('/'.join(args[0])) - else: - stre = args[0] - DAV_Error.__init__(self,404,stre) - else: - DAV_Error.__init__(self,404) - - pass - -class DAV_Forbidden(DAV_Error): - """ a method on a resource is not allowed """ - - def __init__(self, *args): - if len(args): - DAV_Error.__init__(self,403,args[0]) - else: - DAV_Error.__init__(self,403) - pass - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/iface.py b/addons/caldav/DAV/iface.py deleted file mode 100644 index a723fb9f696..00000000000 --- a/addons/caldav/DAV/iface.py +++ /dev/null @@ -1,264 +0,0 @@ -""" - -basic interface class - -use this for subclassing when writing your own interface -class. - -""" - -from errors import * - -import time -from string import lower - -class dav_interface: - """ interface class for implementing DAV servers """ - - ### defined properties (modify this but let the DAV stuff there!) - ### the format is namespace: [list of properties] - - PROPS={"DAV:" : ('creationdate', - 'displayname', - 'getcontentlanguage', - 'getcontentlength', - 'getcontenttype', - 'getetag', - 'getlastmodified', - 'lockdiscovery', - 'resourcetype', - 'source', - 'supportedlock'), - "NS2" : ("p1","p2") - } - - # here we define which methods handle which namespace - # the first item is the namespace URI and the second one - # the method prefix - # e.g. for DAV:getcontenttype we call dav_getcontenttype() - M_NS = {"DAV:" : "_get_dav", - "NS2" : "ns2" } - - def get_propnames(self, uri): - """ return the property names allowed for the given URI - - In this method we simply return the above defined properties - assuming that they are valid for any resource. - You can override this in order to return a different set - of property names for each resource. - - """ - return self.PROPS - - def get_prop2(self, uri, ns, pname): - """ return the value of a property - """ - if lower(ns)=="dav:": return self.get_dav(uri,pname) - - raise DAV_NotFound - - def get_prop(self, uri, ns, propname): - """ return the value of a given property - - uri -- uri of the object to get the property of - ns -- namespace of the property - pname -- name of the property - """ - if self.M_NS.has_key(ns): - prefix = self.M_NS[ns] - else: - print "No namespace:",ns, "( for prop:", propname,")" - raise DAV_NotFound - mname = prefix+"_"+propname - if not hasattr(self,mname): - raise DAV_NotFound - - try: - m = getattr(self,mname) - r = m(uri) - return r - except AttributeError, e: - print 'Property %s not supported' % propname - print "Exception:", e - raise DAV_NotFound - - ### - ### DATA methods (for GET and PUT) - ### - - def get_data(self, uri): - """ return the content of an object - - return data or raise an exception - - """ - raise DAV_NotFound - - def put(self, uri, data): - """ write an object to the repository - - return a result code or raise an exception - """ - - raise DAV_Forbidden - - ### - ### Methods for DAV properties - ### - - def _get_dav_creationdate(self, uri): - """ return the creationdate of a resource """ - d = self.get_creationdate(uri) - # format it - if isinstance(d, int) or isinstance(d, float): - d = time.localtimetime(d) - return time.strftime("%Y-%m-%dT%H:%M:%S%Z",d) - - def _get_dav_getlastmodified(self, uri): - """ return the last modified date of a resource """ - d = self.get_lastmodified(uri) - if isinstance(d, int) or isinstance(d, float): - d = time.localtime(d) - # format it - return time.asctime(d) - - - ### - ### OVERRIDE THESE! - ### - - def get_creationdate(self, uri): - """ return the creationdate of the resource """ - return time.time() - - def get_lastmodified(self, uri): - """ return the last modification date of the resource """ - return time.time() - - - ### - ### COPY MOVE DELETE - ### - - ### methods for deleting a resource - - def rmcol(self, uri): - """ delete a collection - - This should not delete any children! This is automatically done - before by the DELETE class in DAV/delete.py - - return a success code or raise an exception - - """ - raise DAV_NotFound - - def rm(self, uri): - """ delete a single resource - - return a success code or raise an exception - - """ - raise DAV_NotFound - - """ - - COPY/MOVE HANDLER - - These handler are called when a COPY or MOVE method is invoked by - a client. In the default implementation it works as follows: - - - the davserver receives a COPY/MOVE method - - the davcopy or davmove module will be loaded and the corresponding - class will be initialized - - this class parses the query and decides which method of the interface class - to call: - - copyone for a single resource to copy - copytree for a tree to copy (collection) - (the same goes for move of course). - - - the interface class has now two options: - 1. to handle the action directly (e.g. cp or mv on filesystems) - 2. to let it handle via the copy/move methods in davcmd. - - ad 1) The first approach can be used when we know that no error can - happen inside a tree or when the action can exactly tell which - element made which error. We have to collect these and return - it in a dict of the form {uri: error_code, ...} - - ad 2) The copytree/movetree/... methods of davcmd.py will do the recursion - themselves and call for each resource the copy/move method of the - interface class. Thus method will then only act on a single resource. - (Thus a copycol on a normal unix filesystem actually only needs to do - an mkdir as the content will be copied by the davcmd.py function. - The davcmd.py method will also automatically collect all errors and - return the dictionary described above. - When you use 2) you also have to implement the copy() and copycol() - methods in your interface class. See the example for details. - - To decide which approach is the best you have to decide if your application - is able to generate errors inside a tree. E.g. a function which completely - fails on a tree if one of the tree's childs fail is not what we need. Then - 2) would be your way of doing it. - Actually usually 2) is the better solution and should only be replaced by - 1) if you really need it. - - The remaining question is if we should do the same for the DELETE method. - - """ - - ### MOVE handlers - - def moveone(self, src, dst, overwrite): - """ move one resource with Depth=0 """ - return moveone(self, src, dst, overwrite) - - def movetree(self, src, dst, overwrite): - """ move a collection with Depth=infinity """ - return movetree(self, src, dst, overwrite) - - ### COPY handlers - - def copyone(self, src, dst, overwrite): - """ copy one resource with Depth=0 """ - return copyone(self, src, dst, overwrite) - - def copytree(self, src, dst, overwrite): - """ copy a collection with Depth=infinity """ - return copytree(self, src, dst, overwrite) - - - ### low level copy methods (you only need these for method 2) - def copy(self, src, dst): - """ copy a resource with depth==0 - - You don't need to bother about overwrite or not. - This has been done already. - - return a success code or raise an exception if something fails - """ - return 201 - - - def copycol(self, src, dst): - """ copy a resource with depth==infinity - - You don't need to bother about overwrite or not. - This has been done already. - - return a success code or raise an exception if something fails - """ - return 201 - - ### some utility functions you need to implement - - def exists(self, uri): - """ return 1 or None depending on if a resource exists """ - return None # no - - def is_collection(self, uri): - """ return 1 or None depending on if a resource is a collection """ - return None # no - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/propfind.py b/addons/caldav/DAV/propfind.py deleted file mode 100644 index 4d009e7912e..00000000000 --- a/addons/caldav/DAV/propfind.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/env python - -""" - python davserver - Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library 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 - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public - License along with this library; if not, write to the Free - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -""" - -from xml.dom import ext -from xml.dom.Document import Document - -import sys -import string -import urlparse -import urllib -from StringIO import StringIO - -import utils -from constants import COLLECTION, OBJECT, DAV_PROPS, RT_ALLPROP, RT_PROPNAME, RT_PROP -from errors import * - -def utf8str(st): - if isinstance(st,unicode): - return st.encode('utf8') - else: - return str(st) - -class PROPFIND: - """ parse a propfind xml element and extract props - - It will set the following instance vars: - - request_class: ALLPROP | PROPNAME | PROP - proplist: list of properties - nsmap: map of namespaces - - The list of properties will contain tuples of the form - (element name, ns_prefix, ns_uri) - - - """ - - def __init__(self, uri, dataclass, depth): - self.request_type = None - self.nsmap = {} - self.proplist = {} - self.default_ns = None - self.__dataclass = dataclass - self.__depth = str(depth) - self.__uri = uri - self.use_full_urls = True - self.__has_body = None # did we parse a body? - - def read_propfind(self, xml_doc): - self.request_type,self.proplist,self.namespaces = utils.parse_propfind(xml_doc) - - # a violation of the expected logic: client (korganizer) will ask for DAV:resourcetype - # but we also have to return the http://groupdav.org/:resourcetype property! - if self.proplist.has_key('DAV:') and 'resourcetype' in self.proplist['DAV:']: - if not self.proplist.has_key('http://groupdav.org/'): - self.proplist['http://groupdav.org/'] = [] - self.proplist['http://groupdav.org/'].append('resourcetype') - if 'DAV:' in self.namespaces: #TMP - self.namespaces.append('http://groupdav.org/') - - def createResponse(self): - """ create the multistatus response - - This will be delegated to the specific method - depending on which request (allprop, propname, prop) - was found. - - If we get a PROPNAME then we simply return the list with empty - values which we get from the interface class - - If we get an ALLPROP we first get the list of properties and then - we do the same as with a PROP method. - - If the uri doesn't exist, return an xml response with a 404 status - - """ - - if not self.__dataclass.exists(self.__uri): - raise DAV_NotFound("Path %s doesn't exist" % self.__uri) - - if self.request_type==RT_ALLPROP: - return self.create_allprop() - - if self.request_type==RT_PROPNAME: - return self.create_propname() - - if self.request_type==RT_PROP: - return self.create_prop() - - # no body means ALLPROP! - return self.create_allprop() - - def create_propname(self): - """ create a multistatus response for the prop names """ - - dc = self.__dataclass - # create the document generator - doc = Document(None) - ms = doc.createElement("D:multistatus") - ms.setAttribute("xmlns:D","DAV:") - doc.appendChild(ms) - - if self.__depth=="0": - pnames = dc.get_propnames(self.__uri) - re = self.mk_propname_response(self.__uri,pnames,doc) - ms.appendChild(re) - - elif self.__depth=="1": - pnames = dc.get_propnames(self.__uri) - re = self.mk_propname_response(self.__uri,pnames,doc) - ms.appendChild(re) - - for newuri in dc.get_childs(self.__uri): - pnames = dc.get_propnames(newuri) - re = self.mk_propname_response(newuri,pnames,doc) - ms.appendChild(re) - # *** depth=="infinity" - - sfile = StringIO() - ext.PrettyPrint(doc,stream = sfile) - s = sfile.getvalue() - sfile.close() - return s - - def create_allprop(self): - """ return a list of all properties """ - self.proplist = {} - self.namespaces = [] - for ns,plist in self.__dataclass.get_propnames(self.__uri).items(): - self.proplist[ns] = plist - self.namespaces.append(ns) - - return self.create_prop() - - def create_prop(self): - """ handle a request - - This will - - 1. set up the -Framework - - 2. read the property values for each URI - (which is dependant on the Depth header) - This is done by the get_propvalues() method. - - 3. For each URI call the append_result() method - to append the actual -Tag to the result - document. - - We differ between "good" properties, which have been - assigned a value by the interface class and "bad" - properties, which resulted in an error, either 404 - (Not Found) or 403 (Forbidden). - - """ - - - # create the document generator - doc = Document(None) - ms = doc.createElement("D:multistatus") - ms.setAttribute("xmlns:D","DAV:") - doc.appendChild(ms) - - if self.__depth=="0": - gp,bp = self.get_propvalues(self.__uri) - res = self.mk_prop_response(self.__uri,gp,bp,doc) - ms.appendChild(res) - - elif self.__depth=="1": - gp,bp = self.get_propvalues(self.__uri) - res = self.mk_prop_response(self.__uri,gp,bp,doc) - ms.appendChild(res) - - try: - for newuri in self.__dataclass.get_childs(self.__uri): - gp,bp = self.get_propvalues(newuri) - res = self.mk_prop_response(newuri,gp,bp,doc) - ms.appendChild(res) - except DAV_NotFound: - # If no children, never mind. - pass - - sfile = StringIO() - ext.PrettyPrint(doc,stream = sfile) - s = sfile.getvalue() - sfile.close() - return s - - - def mk_propname_response(self,uri,propnames,doc): - """ make a new result element for a PROPNAME request - - This will simply format the propnames list. - propnames should have the format {NS1 : [prop1, prop2, ...], NS2: ...} - - """ - re = doc.createElement("D:response") - - # write href information - href = doc.createElement("D:href") - if self.use_full_urls: - huri = doc.createTextNode(uri) - else: - uparts = urlparse.urlparse(uri) - fileloc = uparts[2] - huri = doc.createTextNode(urllib.quote(fileloc.encode('utf8'))) - href.appendChild(huri) - re.appendChild(href) - - ps = doc.createElement("D:propstat") - nsnum = 0 - - for ns,plist in propnames.items(): - # write prop element - pr = doc.createElement("D:prop") - nsp = "ns"+str(nsnum) - pr.setAttribute("xmlns:"+nsp,ns) - nsnum = nsnum+1 - - # write propertynames - for p in plist: - pe = doc.createElement(nsp+":"+p) - pr.appendChild(pe) - - ps.appendChild(pr) - - re.appendChild(ps) - - return re - - def mk_prop_response(self,uri,good_props,bad_props,doc): - """ make a new result element - - We differ between the good props and the bad ones for - each generating an extra -Node (for each error - one, that means). - - """ - re = doc.createElement("D:response") - # append namespaces to response - nsnum = 0 - for nsname in self.namespaces: - re.setAttribute("xmlns:ns"+str(nsnum),nsname) - nsnum = nsnum+1 - - # write href information - href = doc.createElement("D:href") - if self.use_full_urls: - huri = doc.createTextNode(uri) - else: - uparts = urlparse.urlparse(uri) - fileloc = uparts[2] - huri = doc.createTextNode(urllib.quote(fileloc.encode('utf8'))) - href.appendChild(huri) - re.appendChild(href) - - # write good properties - if good_props and len(good_props.items()): - ps = doc.createElement("D:propstat") - - gp = doc.createElement("D:prop") - for ns in good_props.keys(): - ns_prefix="ns"+str(self.namespaces.index(ns))+":" - for p,v in good_props[ns].items(): - pe = doc.createElement(ns_prefix+str(p)) - if v == None: - pass - elif ns=='DAV:' and p=="resourcetype": - if v == 1: - ve=doc.createElement("D:collection") - pe.appendChild(ve) - elif isinstance(v,tuple) and v[1] == ns: - ve=doc.createElement(ns_prefix+v[0]) - pe.appendChild(ve) - else: - ve = doc.createTextNode(utf8str(v)) - pe.appendChild(ve) - - gp.appendChild(pe) - if gp.hasChildNodes(): - re.appendChild(ps) - 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! - if len(bad_props.items()): - - # write a propstat for each error code - for ecode in bad_props.keys(): - ps = doc.createElement("D:propstat") - re.appendChild(ps) - bp = doc.createElement("D:prop") - ps.appendChild(bp) - - for ns in bad_props[ecode].keys(): - ns_prefix = "ns"+str(self.namespaces.index(ns))+":" - - for p in bad_props[ecode][ns]: - 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 - return re - - def get_propvalues(self,uri): - """ create lists of property values for an URI - - We create two lists for an URI: the properties for - which we found a value and the ones for which we - only got an error, either because they haven't been - found or the user is not allowed to read them. - - """ - good_props = {} - bad_props = {} - - for (ns,plist) in self.proplist.items(): - good_props[ns] = {} - bad_props = {} - ec = 0 - for prop in plist: - try: - ec = 0 - r = self.__dataclass.get_prop(uri,ns,prop) - good_props[ns][prop] = r - except DAV_Error, error_code: - ec = error_code[0] - - # ignore props with error_code if 0 (invisible) - if ec==0: continue - - if bad_props.has_key(ec): - if bad_props[ec].has_key(ns): - bad_props[ec][ns].append(prop) - else: - bad_props[ec][ns] = [prop] - else: - bad_props[ec] = {ns:[prop]} - - return good_props, bad_props - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/DAV/status.py b/addons/caldav/DAV/status.py deleted file mode 100644 index 62cb323c4fb..00000000000 --- a/addons/caldav/DAV/status.py +++ /dev/null @@ -1,31 +0,0 @@ -""" - -status codes for DAV services - - -""" - - -STATUS_CODES={ - 102: "Processing", - 200: "Ok", - 201: "Created", - 204: "No Content", - 207: "Multi-Status", - 201: "Created", - 400: "Bad Request", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 409: "Conflict", - 412: "Precondition failed", - 423: "Locked", - 415: "Unsupported Media Type", - 507: "Insufficient Storage", - 422: "Unprocessable Entity", - 423: "Locked", - 424: "Failed Dependency", - 502: "Bad Gateway", - 507: "Insufficient Storage", - 999: "Some error in Create Method please check the data of create method" -} diff --git a/addons/caldav/DAV/utils.py b/addons/caldav/DAV/utils.py deleted file mode 100644 index a4010f27a77..00000000000 --- a/addons/caldav/DAV/utils.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python - -""" - -UTILITIES - -- parse a propfind request body into a list of props - -""" - -from xml.dom import ext -from xml.dom.Document import Document -from xml.dom.ext.reader import PyExpat -from xml.dom import Node -from xml.dom import NodeIterator, NodeFilter - -from string import lower, split, atoi, joinfields -import urlparse -from StringIO import StringIO - -from constants import RT_ALLPROP, RT_PROPNAME, RT_PROP -from status import STATUS_CODES - -VERSION = '0.6' -AUTHOR = 'Simon Pamies ' - - -def gen_estring(ecode): - """ generate an error string from the given code """ - ec = atoi(str(ecode)) - if STATUS_CODES.has_key(ec): - return "HTTP/1.1 %s %s" %(ec,STATUS_CODES[ec]) - else: - return "HTTP/1.1 %s" %(ec) - -def parse_propfind(xml_doc): - """ parse an propfind xml file and return a list of props - - returns: - - request_type -- ALLPROP, PROPNAME, PROP - proplist -- list of properties found - namespaces -- list of namespaces found - - """ - doc = PyExpat.Reader().fromString(xml_doc) - snit = doc.createNodeIterator(doc, NodeFilter.NodeFilter.SHOW_ELEMENT, None, None) - - request_type = None - props = {} - namespaces = [] - - while 1: - curr_elem = snit.nextNode() - if not curr_elem: break - ename=fname = lower(curr_elem.nodeName) - if ":" in fname: - ename = split(fname,":")[1] - if ename=="prop": request_type = RT_PROP; continue - if ename=="propfind": continue - if ename=="allprop": request_type = RT_ALLPROP; continue - if ename=="propname": request_type = RT_PROPNAME; continue - - # rest should be names of attributes - - ns = curr_elem.namespaceURI - if props.has_key(ns): - props[ns].append(ename) - else: - props[ns] = [ename] - namespaces.append(ns) - - return request_type,props,namespaces - - -def create_treelist(dataclass, uri): - """ create a list of resources out of a tree - - This function is used for the COPY, MOVE and DELETE methods - - uri - the root of the subtree to flatten - - It will return the flattened tree as list - - """ - queue = [uri] - list = [uri] - while len(queue): - element = queue[-1] - if dataclass.is_collection(element): - childs = dataclass.get_childs(element) - else: - childs = [] - if len(childs): - list = list+childs - # update queue - del queue[-1] - if len(childs): - queue = queue+childs - return list - -def is_prefix(uri1, uri2): - """ returns 1 of uri1 is a prefix of uri2 """ - if uri2[:len(uri1)]==uri1: - return 1 - else: - return None - -def quote_uri(uri): - """ quote an URL but not the protocol part """ - import urlparse - import urllib - - up = urlparse.urlparse(uri) - np = urllib.quote(up[2]) - return urlparse.urlunparse((up[0], up[1], np, up[3], up[4], up[5])) - -def get_uriparentpath(uri): - """ extract the uri path and remove the last element """ - up = urlparse.urlparse(uri) - return joinfields(split(up[2], "/")[:-1], "/") - -def get_urifilename(uri): - """ extract the uri path and return the last element """ - up = urlparse.urlparse(uri) - return split(up[2], "/")[-1] - -def get_parenturi(uri): - """ return the parent of the given resource""" - up = urlparse.urlparse(uri) - np = joinfields(split(up[2], "/")[:-1], "/") - return urlparse.urlunparse((up[0], up[1], np, up[3], up[4], up[5])) - -### XML utilities - -def make_xmlresponse(result): - """ construct a response from a dict of uri:error_code elements """ - doc = Document(None) - ms = doc.createElement("D:multistatus") - ms.setAttribute("xmlns:D", "DAV:") - doc.appendChild(ms) - - for el, ec in result.items(): - re = doc.createElement("D:response") - hr = doc.createElement("D:href") - st = doc.createElement("D:status") - huri = doc.createTextNode(quote_uri(el)) - t = doc.createTextNode(gen_estring(ec)) - st.appendChild(t) - hr.appendChild(huri) - re.appendChild(hr) - re.appendChild(st) - ms.appendChild(re) - - sfile = StringIO() - ext.PrettyPrint(doc, stream = sfile) - s = sfile.getvalue() - sfile.close() - return s - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/__init__.py b/addons/caldav/__init__.py index 9e222cf99a8..3c7bf49a8fa 100644 --- a/addons/caldav/__init__.py +++ b/addons/caldav/__init__.py @@ -19,11 +19,9 @@ # ############################################################################## +import calendar +import calendar_collection import caldav -import caldav_cache -import caldav_fs -import caldav_node -import webdav_server import wizard # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/caldav/__openerp__.py b/addons/caldav/__openerp__.py index 1d25ad8cb77..3717ad32d83 100644 --- a/addons/caldav/__openerp__.py +++ b/addons/caldav/__openerp__.py @@ -25,6 +25,7 @@ "version" : "1.0", "depends" : [ "base", + "document_webdav", ], 'description': """ This module Contains basic functionality for caldav system like: @@ -34,7 +35,7 @@ - Provides iCal Import/Export functionality To access OpenERP Calendar using caldav to remote site use the URL like: - http://HOSTNAME:PORT/calendar/DATABASE_NAME/CALENDAR_NAME.ics + http://HOSTNAME:PORT/webdav/DATABASE_NAME/Calendars/CALENDAR_NAME.ics Where, HOSTNAME: Host on which OpenERP server(With webdav) is running diff --git a/addons/caldav/caldav.py b/addons/caldav/caldav.py index c5534afa649..f760421dfe5 100644 --- a/addons/caldav/caldav.py +++ b/addons/caldav/caldav.py @@ -19,1009 +19,26 @@ # ############################################################################## -from datetime import datetime, timedelta -from dateutil import parser -from dateutil.rrule import * -from osv import osv, fields -from tools.translate import _ -import base64 -import math -import pooler -import pytz -import re +from document_webdav import webdav import tools -import time - - -try: - import vobject -except ImportError: - raise osv.except_osv('vobject Import Error!','Please install python-vobject \ - from http://vobject.skyhouseconsulting.com/') - -# O-1 Optional and can come only once -# O-n Optional and can come more than once -# R-1 Required and can come only once -# R-n Required and can come more than once - -def uid2openobjectid(cr, uidval, oomodel, rdate): - """ UID To Open Object Id - @param cr: the current row, from the database cursor, - @param uidval: Get USerId vale - @oomodel: Open Object ModelName - @param rdate: Get Recurrent Date - """ - __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$') - wematch = __rege.match(uidval.encode('utf8')) - if not wematch: - return (False, None) - else: - model, id, dbname = wematch.groups() - model_obj = pooler.get_pool(cr.dbname).get(model) - if (not model == oomodel) or (not dbname == cr.dbname): - return (False, None) - qry = 'select distinct(id) from %s' % model_obj._table - if rdate: - qry += " where recurrent_id='%s'" % (rdate) - cr.execute(qry) - r_id = cr.fetchone() - if r_id: - return (id, r_id[0]) - cr.execute(qry) - ids = map(lambda x: str(x[0]), cr.fetchall()) - if id in ids: - return (id, None) - return (False, None) - -def openobjectid2uid(cr, uidval, oomodel): - """ Open Object Id To UId - @param cr: the current row, from the database cursor, - @param uidval: Get USerId vale - @oomodel: Open Object ModelName """ - - value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname) - return value - -def get_attribute_mapping(cr, uid, calname, context={}): - """ Attribute Mapping with Basic calendar fields and lines - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param calname: Get Calendar name - @param context: A standard dictionary for contextual values """ - - if not context: - context = {} - pool = pooler.get_pool(cr.dbname) - field_obj = pool.get('basic.calendar.fields') - type_obj = pool.get('basic.calendar.lines') - domain = [('object_id.model', '=', context.get('model'))] - if context.get('calendar_id'): - domain.append(('calendar_id', '=', context.get('calendar_id'))) - type_id = type_obj.search(cr, uid, domain) - fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])]) - res = {} - for field in field_obj.browse(cr, uid, fids): - attr = field.name.name - res[attr] = {} - res[attr]['field'] = field.field_id.name - res[attr]['type'] = field.field_id.ttype - if field.fn == 'hours': - res[attr]['type'] = "timedelta" - if res[attr]['type'] in ('one2many', 'many2many', 'many2one'): - res[attr]['object'] = field.field_id.relation - elif res[attr]['type'] in ('selection') and field.mapping: - res[attr]['mapping'] = eval(field.mapping) - if not res.get('uid', None): - res['uid'] = {} - res['uid']['field'] = 'id' - res['uid']['type'] = "integer" +from DAV.propfind import PROPFIND +import urlparse +urlparse.uses_netloc.append('caldav') +urlparse.uses_netloc.append('caldavs') +super_mk_prop_response = webdav.mk_prop_response +def mk_prop_response(self,uri,good_props,bad_props,doc): + res = super_mk_prop_response(self, uri,good_props,bad_props,doc) + uris = uri.split('/') + calendar = False + if 'http://calendarserver.org/ns/' in good_props or 'http://calendarserver.org/ns/' in bad_props: + calendar = True + if calendar: + ad = doc.createElement('calendar') + ad.setAttribute('xmlns', 'urn:ietf:params:xml:ns:caldav') + cols = res.getElementsByTagName('D:collection') + if cols: + cols[0].parentNode.appendChild(ad) + #cols[0].parentNode.appendChild(vc) return res -def map_data(cr, uid, obj): - """ Map Data - @param self: The object pointer - @param cr: the current row, from the database cursor,""" - - vals = {} - for map_dict in obj.__attribute__: - map_val = obj.ical_get(map_dict, 'value') - field = obj.ical_get(map_dict, 'field') - field_type = obj.ical_get(map_dict, 'type') - if field: - if field_type == 'selection': - if not map_val: - continue - mapping = obj.__attribute__[map_dict].get('mapping', False) - if mapping: - map_val = mapping[map_val.lower()] - else: - map_val = map_val.lower() - if field_type == 'many2many': - ids = [] - if not map_val: - vals[field] = ids - continue - model = obj.__attribute__[map_dict].get('object', False) - modobj = obj.pool.get(model) - for map_vall in map_val: - id = modobj.create(cr, uid, map_vall) - ids.append(id) - vals[field] = [(6, 0, ids)] - continue - if field_type == 'many2one': - id = None - if not map_val or not isinstance(map_val, dict): - vals[field] = id - continue - model = obj.__attribute__[map_dict].get('object', False) - modobj = obj.pool.get(model) - id = modobj.create(cr, uid, map_val) - vals[field] = id - continue - if field_type == 'timedelta': - if map_val: - vals[field] = (map_val.seconds/float(86400) + map_val.days) - if map_val: - vals[field] = map_val - return vals - -class CalDAV(object): - __attribute__ = {} - - - - def ical_set(self, name, value, type): - """ set calendar Attribute - @param self: The object pointer, - @param name: Get Attribute Name - @param value: Get Attribute Value - @param type: Get Attribute Type - """ - if name in self.__attribute__ and self.__attribute__[name]: - self.__attribute__[name][type] = value - return True - - def ical_get(self, name, type): - """ Get calendar Attribute - @param self: The object pointer, - @param name: Get Attribute Name - @param type: Get Attribute Type - """ - - if self.__attribute__.get(name): - val = self.__attribute__.get(name).get(type, None) - valtype = self.__attribute__.get(name).get('type', None) - if type == 'value': - if valtype and valtype == 'datetime' and val: - if isinstance(val, list): - val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val)) - else: - val = val.strftime('%Y-%m-%d %H:%M:%S') - return val - else: - return self.__attribute__.get(name, None) - - def ical_reset(self, type): - """ Reset Calendar Attribute - @param self: The object pointer, - @param type: Get Attribute Type - """ - - for name in self.__attribute__: - if self.__attribute__[name]: - self.__attribute__[name][type] = None - return True - - def parse_ics(self, cr, uid, child, cal_children=None, context=None): - """ parse calendaring and scheduling information - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values """ - - att_data = [] - for cal_data in child.getChildren(): - if cal_data.name.lower() == 'attendee': - ctx = context.copy() - if cal_children: - ctx.update({'model': cal_children[cal_data.name.lower()]}) - attendee = self.pool.get('basic.calendar.attendee') - att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx)) - self.ical_set(cal_data.name.lower(), att_data, 'value') - continue - if cal_data.name.lower() == 'valarm': - alarm = self.pool.get('basic.calendar.alarm') - ctx = context.copy() - if cal_children: - ctx.update({'model': cal_children[cal_data.name.lower()]}) - vals = alarm.import_cal(cr, uid, cal_data, context=ctx) - self.ical_set(cal_data.name.lower(), vals, 'value') - continue - if cal_data.name.lower() == 'exdate': - exval = map(lambda x: str(x), cal_data.value) - self.ical_set(cal_data.name.lower(), ','.join(exval), 'value') - continue - 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') - self.ical_set(cal_data.name.lower(), cal_data.value, 'value') - vals = map_data(cr, uid, self) - return vals - - def create_ics(self, cr, uid, datas, name, ical, context=None): - """ create calendaring and scheduling information - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values """ - - if not datas: - return - timezones = [] - for data in datas: - tzval = None - vevent = ical.add(name) - for field in self.__attribute__.keys(): - map_field = self.ical_get(field, 'field') - map_type = self.ical_get(field, 'type') - if map_field in data.keys(): - if field == 'uid': - model = context.get('model', None) - if not model: - continue - uidval = openobjectid2uid(cr, data[map_field], model) - model_obj = self.pool.get(model) - r_ids = [] - if model_obj._columns.get('recurrent_uid', None): - cr.execute('select id from %s where recurrent_uid=%s' - % (model_obj._table, data[map_field])) - r_ids = map(lambda x: x[0], cr.fetchall()) - if r_ids: - rdata = self.pool.get(model).read(cr, uid, r_ids) - event_obj = self.pool.get('basic.calendar.event') - rcal = event_obj.export_cal(cr, uid, rdata, context=context) - for revents in rcal.contents['vevent']: - ical.contents['vevent'].append(revents) - if data.get('recurrent_uid', None): - uidval = openobjectid2uid(cr, data['recurrent_uid'], model) - vevent.add('uid').value = uidval - elif field == 'attendee' and data[map_field]: - model = self.__attribute__[field].get('object', False) - attendee_obj = self.pool.get('basic.calendar.attendee') - vevent = attendee_obj.export_cal(cr, uid, model, \ - data[map_field], vevent, context=context) - elif field == 'valarm' and data[map_field]: - model = self.__attribute__[field].get('object', False) - ctx = context.copy() - ctx.update({'model': model}) - alarm_obj = self.pool.get('basic.calendar.alarm') - vevent = alarm_obj.export_cal(cr, uid, model, \ - data[map_field][0], vevent, context=ctx) - elif field == 'vtimezone' and data[map_field] and data[map_field] not in timezones: - tzval = data[map_field] - tz_obj = self.pool.get('basic.calendar.timezone') - ical = tz_obj.export_cal(cr, uid, None, \ - data[map_field], ical, context=context) - timezones.append(data[map_field]) - elif data[map_field]: - if map_type in ("char", "text"): - if field in ('exdate'): - vevent.add(field).value = map(parser.parse, (data[map_field]).split(',')) - else: - vevent.add(field).value = tools.ustr(data[map_field]) - elif map_type in ('datetime', 'date') and data[map_field]: - dtfield = vevent.add(field) - dtfield.value = parser.parse(data[map_field]) - if tzval: - dtfield.params['TZID'] = [tzval.title()] - elif map_type == "timedelta": - vevent.add(field).value = timedelta(hours=data[map_field]) - elif map_type == "many2one": - vevent.add(field).value = tools.ustr(data.get(map_field)[1]) - elif map_type in ("float", "integer"): - vevent.add(field).value = str(data.get(map_field)) - elif map_type == "selection": - if not self.ical_get(field, 'mapping'): - vevent.add(field).value = (tools.ustr(data[map_field])).upper() - else: - for key1, val1 in self.ical_get(field, 'mapping').items(): - if val1 == data[map_field]: - vevent.add(field).value = key1 - return vevent - - def check_import(self, cr, uid, vals, context={}): - """ - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param vals: Get Values - @param context: A standard dictionary for contextual values - """ - - ids = [] - model_obj = self.pool.get(context.get('model')) - recur_pool = {} - try: - for val in vals: - exists, r_id = uid2openobjectid(cr, val['id'], context.get('model'), \ - val.get('recurrent_id')) - if val.has_key('create_date'): val.pop('create_date') - u_id = val.get('id', None) - val.pop('id') - if exists and r_id: - val.update({'recurrent_uid': exists}) - model_obj.write(cr, uid, [r_id], val) - ids.append(r_id) - elif exists: - model_obj.write(cr, uid, [exists], val) - ids.append(exists) - else: - if u_id in recur_pool and val.get('recurrent_id'): - val.update({'recurrent_uid': recur_pool[u_id]}) - revent_id = model_obj.create(cr, uid, val) - ids.append(revent_id) - else: - event_id = model_obj.create(cr, uid, val) - recur_pool[u_id] = event_id - ids.append(event_id) - except Exception, e: - raise osv.except_osv(('Error !'), (str(e))) - return ids - - def export_cal(self, cr, uid, datas, vobj=None, context={}): - """ Export Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param datas: Get Data's for caldav - @param context: A standard dictionary for contextual values - """ - - try: - self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context) - ical = vobject.iCalendar() - self.create_ics(cr, uid, datas, vobj, ical, context=context) - return ical - except Exception, e: - raise osv.except_osv(('Error !'), (str(e))) - - def import_cal(self, cr, uid, content, data_id=None, context=None): - """ Import Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param data_id: Get Data’s ID or False - @param context: A standard dictionary for contextual values - """ - - ical_data = base64.decodestring(content) - self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context) - parsedCal = vobject.readOne(ical_data) - res = [] - vals = {} - for child in parsedCal.getChildren(): - if child.name.lower() in ('vevent', 'vtodo'): - vals = self.parse_ics(cr, uid, child, context=context) - else: - vals = {} - continue - if vals: res.append(vals) - self.ical_reset('value') - return res - - -class Calendar(CalDAV, osv.osv): - _name = 'basic.calendar' - _description = 'Calendar' - _calname = 'calendar' - - __attribute__ = { - 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object. - 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number - # or the minimum and maximum range of the iCalendar specification - # that is required in order to interpret the iCalendar object. - 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object. - 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object. - 'vevent': None, # Use: O-n, Type: Collection of Event class - 'vtodo': None, # Use: O-n, Type: Collection of ToDo class - 'vjournal': None, # Use: O-n, Type: Collection of Journal class - 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class - 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class - } - _columns = { - 'name': fields.char("Name", size=64), - 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'), - 'active': fields.boolean('Active'), - 'create_date': fields.datetime('Created Date'), - 'write_date': fields.datetime('Modifided Date'), - } - - _defaults = { - 'active': lambda *a: True, - } - - def export_cal(self, cr, uid, ids, vobj='vevent', context={}): - """ Export Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of calendar’s IDs - @param context: A standard dictionary for contextual values - """ - - cal = self.browse(cr, uid, ids[0]) - ical = vobject.iCalendar() - for line in cal.line_ids: - if line.name in ('valarm', 'attendee'): - continue - mod_obj = self.pool.get(line.object_id.model) - data_ids = mod_obj.search(cr, uid, eval(line.domain), context=context) - datas = mod_obj.read(cr, uid, data_ids, context=context) - context.update({'model': line.object_id.model, - 'calendar_id': cal.id - }) - self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context) - self.create_ics(cr, uid, datas, line.name, ical, context=context) - return ical.serialize() - - def import_cal(self, cr, uid, content, data_id=None, context=None): - """ Import Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param data_id: Get Data’s ID or False - @param context: A standard dictionary for contextual values - """ - - if not context: - context = {} - vals = [] - ical_data = base64.decodestring(content) - parsedCal = vobject.readOne(ical_data) - if not data_id: - data_id = self.search(cr, uid, [])[0] - cal = self.browse(cr, uid, data_id) - cal_children = {} - count = 0 - for line in cal.line_ids: - cal_children[line.name] = line.object_id.model - for child in parsedCal.getChildren(): - if child.name.lower() in cal_children: - context.update({'model': cal_children[child.name.lower()], - 'calendar_id': cal.id - }) - self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context) - val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context) - vals.append(val) - obj = self.pool.get(cal_children[child.name.lower()]) - if hasattr(obj, 'check_import'): - obj.check_import(cr, uid, vals, context=context) - else: - self.check_import(cr, uid, vals, context=context) - return {} -Calendar() - - -class basic_calendar_line(osv.osv): - """ Calendar Lines """ - - _name = 'basic.calendar.lines' - _description = 'Calendar Lines' - - _columns = { - 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \ - ('valarm', 'Alarm'), \ - ('attendee', 'Attendee')], \ - string="Type", size=64), - 'object_id': fields.many2one('ir.model', 'Object'), - 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \ - required=True, ondelete='cascade'), - 'domain': fields.char('Domain', size=124), - 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping') - } - - _defaults = { - 'domain': lambda *a: '[]', - } - - def create(self, cr, uid, vals, context={}): - """ create calendar's line - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param vals: Get the Values - @param context: A standard dictionary for contextual values - """ - - cr.execute("Select count(id) from basic_calendar_lines \ - where name='%s' and calendar_id=%s" % (vals.get('name'), vals.get('calendar_id'))) - res = cr.fetchone() - if res: - if res[0] > 0: - raise osv.except_osv(_('Warning !'), _('Can not create \ -line "%s" more than once' % (vals.get('name')))) - return super(basic_calendar_line, self).create(cr, uid, vals, context=context) - -basic_calendar_line() - - -class basic_calendar_attribute(osv.osv): - _name = 'basic.calendar.attributes' - _description = 'Calendar attributes' - _columns = { - 'name': fields.char("Name", size=64, required=True), - 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \ - ('alarm', 'Alarm'), \ - ('attendee', 'Attendee')], \ - string="Type", size=64, required=True), - } - -basic_calendar_attribute() - - -class basic_calendar_fields(osv.osv): - """ Calendar fields """ - - _name = 'basic.calendar.fields' - _description = 'Calendar fields' - - _columns = { - 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'), - 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True), - 'type_id': fields.many2one('basic.calendar.lines', 'Type', \ - required=True, ondelete='cascade'), - 'expr': fields.char("Expression", size=64), - 'fn': fields.selection([('field', 'Use the field'), - ('const', 'Expression as constant'), - ('hours', 'Interval in hours'), - ], 'Function'), - 'mapping': fields.text('Mapping'), - } - - _defaults = { - 'fn': lambda *a: 'field', - } - - def check_line(self, cr, uid, vals, name, context=None): - """ check calendar's line - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param vals: Get Values - @param context: A standard dictionary for contextual values - """ - f_obj = self.pool.get('ir.model.fields') - field = f_obj.browse(cr, uid, vals['field_id'], context=context) - relation = field.relation - line_obj = self.pool.get('basic.calendar.lines') - l_id = line_obj.search(cr, uid, [('name', '=', name)]) - if l_id: - line = line_obj.browse(cr, uid, l_id, context=context)[0] - line_rel = line.object_id.model - if (relation != 'NULL') and (not relation == line_rel): - raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name))) - return True - - def create(self, cr, uid, vals, context={}): - """ Create Calendar's fields - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param vals: Get Values - @param context: A standard dictionary for contextual values - """ - - cr.execute('select name from basic_calendar_attributes \ - where id=%s' % (vals.get('name'))) - name = cr.fetchone() - name = name[0] - if name in ('valarm', 'attendee'): - self.check_line(cr, uid, vals, name, context=context) - cr.execute("Select count(id) from basic_calendar_fields \ - where name=%s and type_id=%s" % (vals.get('name'), vals.get('type_id'))) - res = cr.fetchone() - if res: - if res[0] > 0: - raise osv.except_osv(_('Warning !'), _('Can not map the field more than once')) - return super(basic_calendar_fields, self).create(cr, uid, vals, context=context) - - def write(self, cr, uid, ids, vals, context=None): - """ write Calendar's fields - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param vals: Get Values - @param context: A standard dictionary for contextual values - """ - - if not vals: - return - for id in ids: - field = self.browse(cr, uid, id, context=context) - name = field.name.name - if name in ('valarm', 'attendee'): - self.check_line(cr, uid, vals, name, context=context) - qry = "Select count(id) from basic_calendar_fields \ - where name=%s and type_id=%s" % (field.name.id, field.type_id.id) - cr.execute(qry) - res = cr.fetchone() - if res: - if res[0] > 1: - raise osv.except_osv(_('Warning !'), _('Can not map same field more than once')) - return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context) - -basic_calendar_fields() - - -class Event(CalDAV, osv.osv_memory): - _name = 'basic.calendar.event' - _calname = 'vevent' - __attribute__ = { - 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL" - 'created': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the calendar information was created by the calendar user agent in the calendar store. - 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. - 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins. - 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component. - 'last-mod': None, # Use: O-1, Type: DATE-TIME Specifies the date and time that the information associated with the calendar component was last revised in the calendar store. - 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component. - 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component. - 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component. - 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created. - 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision. - 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component. - 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component. - 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches. - 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component. - 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object. - 'recurid': None, - 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component. - 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component. - 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component. - 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user. - 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component. - 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component. - 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set. - 'rstatus': None, - 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component. - # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M - 'resources': None, # Use: O-n, Type: TEXT, Defines the equipment or resources anticipated for an activity specified by a calendar entity like RESOURCES:EASEL,PROJECTOR,VCR, LANGUAGE=fr:1 raton-laveur - 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set. - 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions. - 'x-prop': None, - 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time. - 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends. - } - - def export_cal(self, cr, uid, datas, vobj='vevent', context={}): - """ Export calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param datas: Get datas - @param context: A standard dictionary for contextual values - """ - - return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context) - -Event() - - -class ToDo(CalDAV, osv.osv_memory): - _name = 'basic.calendar.todo' - _calname = 'vtodo' - - __attribute__ = { - 'class': None, - 'completed': None, - 'created': None, - 'description': None, - 'dtstamp': None, - 'dtstart': None, - 'duration': None, - 'due': None, - 'geo': None, - 'last-mod ': None, - 'location': None, - 'organizer': None, - 'percent': None, - 'priority': None, - 'recurid': None, - 'seq': None, - 'status': None, - 'summary': None, - 'uid': None, - 'url': None, - 'attach': None, - 'attendee': None, - 'categories': None, - 'comment': None, - 'contact': None, - 'exdate': None, - 'exrule': None, - 'rstatus': None, - 'related': None, - 'resources': None, - 'rdate': None, - 'rrule': None, - } - - def export_cal(self, cr, uid, datas, vobj='vevent', context={}): - """ Export Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param datas: Get datas - @param context: A standard dictionary for contextual values - """ - - return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context) - -ToDo() - - -class Journal(CalDAV): - __attribute__ = { - } - - -class FreeBusy(CalDAV): - __attribute__ = { - 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component. - 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins. - 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends. - 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time. - 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created. - 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component. - 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component. - 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object. - 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component. - 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user. - 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals. - 'rstatus': None, - 'X-prop': None, - } - - -class Timezone(CalDAV, osv.osv_memory): - _name = 'basic.calendar.timezone' - _calname = 'vtimezone' - - __attribute__ = { - 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component. - 'last-mod': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the information associated with the calendar component was last revised in the calendar store. - 'tzurl': None, # Use: O-1, Type: URI, Provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to-date version of itself. - 'standardc': {'tzprop': None}, # Use: R-1, - 'daylightc': {'tzprop': None}, # Use: R-1, - 'x-prop': None, # Use: O-n, Type: Text, - } - - def get_name_offset(self, cr, uid, tzid, context={}): - """ Get Name Offset value - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - - mytz = pytz.timezone(tzid.title()) - mydt = datetime.now(tz=mytz) - offset = mydt.utcoffset() - val = offset.days * 24 + float(offset.seconds) / 3600 - realoffset = '%02d%02d' % (math.floor(abs(val)), \ - round(abs(val) % 1 + 0.01, 2) * 60) - realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset)) - return (mydt.tzname(), realoffset) - - def export_cal(self, cr, uid, model, tzid, ical, context={}): - """ Export Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param model: Get Model's name - @param context: A standard dictionary for contextual values - """ - - ctx = context.copy() - ctx.update({'model': model}) - cal_tz = ical.add('vtimezone') - cal_tz.add('TZID').value = tzid.title() - tz_std = cal_tz.add('STANDARD') - tzname, offset = self.get_name_offset(cr, uid, tzid) - tz_std.add("TZOFFSETFROM").value = offset - tz_std.add("TZOFFSETTO").value = offset - tz_std.add("DTSTART").value = datetime.now() # TODO - tz_std.add("TZNAME").value = tzname - return ical - - def import_cal(self, cr, uid, ical_data, context=None): - """ Import Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ical_data: Get calendar's data - @param context: A standard dictionary for contextual values - """ - - for child in ical_data.getChildren(): - if child.name.lower() == 'tzid': - tzname = child.value - self.ical_set(child.name.lower(), tzname, 'value') - vals = map_data(cr, uid, self) - return vals - -Timezone() - - -class Alarm(CalDAV, osv.osv_memory): - _name = 'basic.calendar.alarm' - _calname = 'alarm' - - __attribute__ = { - 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE" - 'description': None, # Type: Text, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. Use:- R-1 for DISPLAY,Use:- R-1 for EMAIL,Use:- R-1 for PROCEDURE - 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL - 'attendee': None, # Use: R-n, Type: CAL-ADDRESS, Contain the email address of attendees to receive the message. It can also include one or more. Use for EMAIL - 'trigger': None, # Use: R-1, Type: DURATION, The "TRIGGER" property specifies a duration prior to the start of an event or a to-do. The "TRIGGER" edge may be explicitly set to be relative to the "START" or "END" of the event or to-do with the "related" parameter of the "TRIGGER" property. The "TRIGGER" property value type can alternatively be set to an absolute calendar date and time of day value. Use for all action like AUDIO, DISPLAY, EMAIL and PROCEDURE - 'duration': None, # Type: DURATION, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY - 'repeat': None, # Type: INTEGER, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY - 'attach': None, # Use:- O-n: which MUST point to a sound resource, which is rendered when the alarm is triggered for AUDIO, Use:- O-n: which are intended to be sent as message attachments for EMAIL, Use:- R-1:which MUST point to a procedure resource, which is invoked when the alarm is triggered for PROCEDURE. - 'x-prop': None, - } - - def export_cal(self, cr, uid, model, alarm_id, vevent, context={}): - """ Export Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param model: Get Model's name - @param alarm_id: Get Alarm's Id - @param context: A standard dictionary for contextual values - """ - - valarm = vevent.add('valarm') - alarm_object = self.pool.get(model) - alarm_data = alarm_object.read(cr, uid, alarm_id, []) - - # Compute trigger data - interval = alarm_data['trigger_interval'] - occurs = alarm_data['trigger_occurs'] - duration = (occurs == 'after' and alarm_data['trigger_duration']) \ - or -(alarm_data['trigger_duration']) - related = alarm_data['trigger_related'] - trigger = valarm.add('TRIGGER') - trigger.params['related'] = [related.upper()] - if interval == 'days': - delta = timedelta(days=duration) - if interval == 'hours': - delta = timedelta(hours=duration) - if interval == 'minutes': - delta = timedelta(minutes=duration) - trigger.value = delta - - # Compute other details - valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP' - valarm.add('ACTION').value = alarm_data['action'] - return vevent - - def import_cal(self, cr, uid, ical_data, context=None): - """ Import Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ical_data: Get calendar's Data - @param context: A standard dictionary for contextual values - """ - - ctx = context.copy() - ctx.update({'model': context.get('model', None)}) - self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) - for child in ical_data.getChildren(): - if child.name.lower() == 'trigger': - seconds = child.value.seconds - days = child.value.days - diff = (days * 86400) + seconds - interval = 'days' - related = 'before' - if not seconds: - duration = abs(days) - related = days > 0 and 'after' or 'before' - elif (abs(diff) / 3600) == 0: - duration = abs(diff / 60) - interval = 'minutes' - related = days >= 0 and 'after' or 'before' - else: - duration = abs(diff / 3600) - interval = 'hours' - related = days >= 0 and 'after' or 'before' - self.ical_set('trigger_interval', interval, 'value') - self.ical_set('trigger_duration', duration, 'value') - self.ical_set('trigger_occurs', related.lower(), 'value') - if child.params: - if child.params.get('related'): - self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value') - else: - self.ical_set(child.name.lower(), child.value.lower(), 'value') - vals = map_data(cr, uid, self) - return vals - -Alarm() - - -class Attendee(CalDAV, osv.osv_memory): - _name = 'basic.calendar.attendee' - _calname = 'attendee' - - __attribute__ = { - 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN". - 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property. - 'role': None, # Use: 0-1 Specify the participation role for the calendar user specified by the property like "CHAIR"/"REQ-PARTICIPANT"/"OPT-PARTICIPANT"/"NON-PARTICIPANT" - 'partstat': None, # Use: 0-1 Specify the participation status for the calendar user specified by the property. like use for VEVENT:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED", use for VTODO:-"NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED"/"COMPLETED"/"IN-PROCESS" and use for VJOURNAL:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED". - 'rsvp': None, # Use: 0-1 Specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value like TRUE / FALSE. - 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation. - 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property. - 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property. - 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property. - 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property. - 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter. - } - - def import_cal(self, cr, uid, ical_data, context=None): - """ Import Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ical_data: Get calendar's Data - @param context: A standard dictionary for contextual values - """ - - ctx = context.copy() - ctx.update({'model': context.get('model', None)}) - self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) - for para in ical_data.params: - if para.lower() == 'cn': - self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \ - ical_data.value, 'value') - else: - self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value') - if not ical_data.params.get('CN'): - self.ical_set('cn', ical_data.value, 'value') - vals = map_data(cr, uid, self) - return vals - - def export_cal(self, cr, uid, model, attendee_ids, vevent, context={}): - """ Export Calendar - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param model: Get model's name - @param attendee_ids: Get Attendee's Id - @param context: A standard dictionary for contextual values - """ - - attendee_object = self.pool.get(model) - ctx = context.copy() - ctx.update({'model': model}) - self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) - for attendee in attendee_object.read(cr, uid, attendee_ids, []): - attendee_add = vevent.add('attendee') - cn_val = '' - for a_key, a_val in self.__attribute__.items(): - if attendee[a_val['field']] and a_val['field'] != 'cn': - if a_val['type'] in ('text', 'char', 'selection'): - attendee_add.params[a_key] = [str(attendee[a_val['field']])] - elif a_val['type'] == 'boolean': - attendee_add.params[a_key] = [str(attendee[a_val['field']])] - if a_val['field'] == 'cn' and attendee[a_val['field']]: - cn_val = [str(attendee[a_val['field']])] - if cn_val: - attendee_add.params['CN'] = cn_val - if not attendee['email']: - raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id')) - elif attendee['email']: - attendee_add.value = 'MAILTO:' + attendee['email'] - return vevent - -Attendee() - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +PROPFIND.mk_prop_response = mk_prop_response diff --git a/addons/caldav/caldav_cache.py b/addons/caldav/caldav_cache.py deleted file mode 100644 index 4e5768c3445..00000000000 --- a/addons/caldav/caldav_cache.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 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 . -# -############################################################################## - -import time -import heapq - -def memoize(maxsize): - """decorator to 'memoize' a function - caching its results""" - def decorating_function(f): - cache = {} # map from key to value - heap = [] # list of keys, in LRU heap - cursize = 0 # because len() is slow - def wrapper(*args): - key = repr(args) - # performance crap - _cache = cache - _heap = heap - _heappop = heapq.heappop - _heappush = heapq.heappush - _time = time.time - _cursize = cursize - _maxsize = maxsize - if not _cache.has_key(key): - if _cursize == _maxsize: - # pop oldest element - (_, oldkey) = _heappop(_heap) - _cache.pop(oldkey) - else: - _cursize += 1 - # insert this element - _cache[key] = f(*args) - _heappush(_heap, (_time(), key)) - wrapper.misses += 1 - else: - wrapper.hits += 1 - return cache[key] - wrapper.__doc__ = f.__doc__ - wrapper.__name__ = f.__name__ - wrapper.hits = wrapper.misses = 0 - return wrapper - return decorating_function - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/caldav_data.xml b/addons/caldav/caldav_data.xml new file mode 100644 index 00000000000..04f5a6004a7 --- /dev/null +++ b/addons/caldav/caldav_data.xml @@ -0,0 +1,380 @@ + + + + + + Calendars + True + + + + + + comment + vevent + + + + uid + vevent + + + + seq + vevent + + + + recurrence-id + vevent + + + + transp + vevent + + + + attendee + vevent + + + + related + vevent + + + + rrule + vevent + + + + dtend + vevent + + + + valarm + vevent + + + + vtimezone + vevent + + + + priority + vevent + + + + location + vevent + + + + exrule + vevent + + + + resources + vevent + + + + rstatus + vevent + + + + status + vevent + + + + exdate + vevent + + + + dtstamp + vevent + + + + description + vevent + + + + rdate + vevent + + + + dtstart + vevent + + + + class + vevent + + + + x-openobject-model + vevent + + + + created + vevent + + + + url + vevent + + + + summary + vevent + + + + contact + vevent + + + + + + + status + vtodo + + + + comment + vtodo + + + + attendee + vtodo + + + + valarm + vtodo + + + + description + vtodo + + + + seq + vtodo + + + + vtimezone + vtodo + + + + url + vtodo + + + + completed + vtodo + + + + percent + vtodo + + + + due + vtodo + + + + summary + vtodo + + + + priority + vtodo + + + + exdate + vtodo + + + + location + vtodo + + + + exrule + vtodo + + + + duration + vtodo + + + + organizer + vtodo + + + + dtstart + vtodo + + + + rrule + vtodo + + + + class + vtodo + + + + uid + vtodo + + + + + + cn + attendee + + + + sent-by + attendee + + + + language + attendee + + + + delegated-from + attendee + + + + member + attendee + + + + cutype + attendee + + + + role + attendee + + + + partstat + attendee + + + + delegated-to + attendee + + + + dir + attendee + + + + rsvp + attendee + + + + + + attendee + alarm + + + + trigger_duration + alarm + + + + description + alarm + + + + attach + alarm + + + + trigger_occurs + alarm + + + + trigger_interval + alarm + + + + summary + alarm + + + + duration + alarm + + + + repeat + alarm + + + + action + alarm + + + + trigger_related + alarm + + + + diff --git a/addons/caldav/caldav_fs.py b/addons/caldav/caldav_fs.py deleted file mode 100644 index 9ab7a2156d6..00000000000 --- a/addons/caldav/caldav_fs.py +++ /dev/null @@ -1,334 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 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 . -# -############################################################################## - -import pooler - -import base64 -import sys -import os -import time -from string import joinfields, split, lower - -from service import security - -import netsvc -import urlparse - -from DAV.constants import COLLECTION, OBJECT -from DAV.errors import * -from DAV.iface import * -import urllib - -from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree -from caldav_cache import memoize -from tools import misc -CACHE_SIZE=20000 - - -class tinydav_handler(dav_interface): - """ - This class models a Tiny ERP interface for the DAV server - """ - PROPS={'DAV:': dav_interface.PROPS['DAV:'], } - - M_NS={ "DAV:": dav_interface.M_NS['DAV:'], } - - def __init__(self, parent, verbose=False): - self.db_name = False - self.parent = parent - self.baseuri = parent.baseuri - - def get_propnames(self, uri): - props = self.PROPS - self.parent.log_message('get propnames: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - cr.close() - return props - node = self.uri2object(cr, uid, pool, uri2) - if node: - props.update(node.get_dav_props(cr)) - cr.close() - return props - - def get_prop(self,uri,ns,propname): - """ return the value of a given property - - uri -- uri of the object to get the property of - ns -- namespace of the property - pname -- name of the property - """ - if self.M_NS.has_key(ns): - return dav_interface.get_prop(self, uri, ns, propname) - - if uri[-1]=='/':uri = uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - cr.close() - raise DAV_NotFound - node = self.uri2object(cr, uid, pool, uri2) - if not node: - cr.close() - raise DAV_NotFound - res = node.get_dav_eprop(cr, ns, propname) - cr.close() - return res - - def urijoin(self,*ajoin): - """ Return the base URI of this request, or even join it with the - ajoin path elements - """ - return self.baseuri+ '/'.join(ajoin) - - def uri2local(self, uri): - uparts = urlparse.urlparse(uri) - reluri = uparts[2] - if reluri and reluri[-1]=="/": - reluri = reluri[:-1] - return reluri - - # - # pos: -1 to get the parent of the uri - # - def get_cr(self, uri): - pdb = self.parent.auth_proxy.last_auth - reluri = self.uri2local(uri) - try: - dbname = reluri.split('/')[2] - except: - dbname = False - if not dbname: - return None, None, None, False, None - if not pdb and dbname: - # if dbname was in our uri, we should have authenticated - # against that. - raise Exception("Programming error") - assert pdb == dbname, " %s != %s" %(pdb, dbname) - user, passwd, dbn2, uid = self.parent.auth_proxy.auth_creds[pdb] - db,pool = pooler.get_db_and_pool(dbname) - cr = db.cursor() - uri2 = reluri.split('/')[3:] - return cr, uid, pool, dbname, uri2 - - def uri2object(self, cr, uid, pool, uri): - if not uid: - return None - return pool.get('basic.calendar').get_calendar_object(cr, uid, uri) - - def get_data(self, uri): - self.parent.log_message('GET: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - try: - if not dbname: - raise DAV_Error, 409 - node = self.uri2object(cr, uid, pool, uri2) - if not node: - raise DAV_NotFound(uri2) - try: - datas = node.get_data(cr, uid) - except TypeError, e: - import traceback - self.parent.log_error("GET typeError: %s", str(e)) - self.parent.log_message("Exc: %s", traceback.format_exc()) - raise DAV_Forbidden - except IndexError, e : - self.parent.log_error("GET IndexError: %s", str(e)) - raise DAV_NotFound(uri2) - except Exception, e: - import traceback - self.parent.log_error("GET exception: %s", str(e)) - self.parent.log_message("Exc: %s", traceback.format_exc()) - raise DAV_Error, 409 - return datas - finally: - cr.close() - - @memoize(CACHE_SIZE) - def _get_dav_resourcetype(self, uri): - """ return type of object """ - self.parent.log_message('get RT: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - try: - if not dbname: - return COLLECTION - node = self.uri2object(cr, uid, pool, uri2) - if not node: - raise DAV_NotFound(uri2) - return OBJECT - finally: - cr.close() - - def _get_dav_displayname(self, uri): - self.parent.log_message('get DN: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - cr.close() - return COLLECTION - node = self.uri2object(cr, uid, pool, uri2) - if not node: - cr.close() - raise DAV_NotFound(uri2) - cr.close() - return node.displayname - - @memoize(CACHE_SIZE) - def _get_dav_getcontentlength(self, uri): - """ return the content length of an object """ - self.parent.log_message('get length: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - result = 0 - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - cr.close() - return '0' - node = self.uri2object(cr, uid, pool, uri2) - if not node: - cr.close() - raise DAV_NotFound(uri2) - result = node.content_length or 0 - cr.close() - return str(result) - - @memoize(CACHE_SIZE) - def _get_dav_getetag(self, uri): - """ return the ETag of an object """ - self.parent.log_message('get etag: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - result = 0 - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - cr.close() - return '0' - node = self.uri2object(cr, uid, pool, uri2) - if not node: - cr.close() - raise DAV_NotFound(uri2) - result = node.get_etag(cr) - cr.close() - return str(result) - - @memoize(CACHE_SIZE) - def get_lastmodified(self, uri): - """ return the last modified date of the object """ - if uri[-1]=='/':uri=uri[:-1] - today = time.time() - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - try: - if not dbname: - return today - node = self.uri2object(cr, uid, pool, uri2) - if not node: - raise DAV_NotFound(uri2) - if node.write_date: - return time.mktime(time.strptime(node.write_date, '%Y-%m-%d %H:%M:%S')) - else: - return today - finally: - cr.close() - - @memoize(CACHE_SIZE) - def get_creationdate(self, uri): - """ return the last modified date of the object """ - - if uri[-1]=='/':uri=uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - try: - if not dbname: - raise DAV_Error, 409 - node = self.uri2object(cr, uid, pool, uri2) - if not node: - raise DAV_NotFound(uri2) - if node.create_date: - result = time.strptime(node.create_date, '%Y-%m-%d %H:%M:%S') - else: - result = time.gmtime() - return result - finally: - cr.close() - - @memoize(CACHE_SIZE) - def _get_dav_getcontenttype(self, uri): - self.parent.log_message('get contenttype: %s' % uri) - if uri[-1]=='/':uri=uri[:-1] - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - try: - if not dbname: - return 'httpd/unix-directory' - node = self.uri2object(cr, uid, pool, uri2) - if not node: - raise DAV_NotFound(uri2) - result = node.mimetype - return result - #raise DAV_NotFound, 'Could not find %s' % path - finally: - cr.close() - - - - def put(self, uri, data, content_type=None): - """ put the object into the filesystem """ - self.parent.log_message('Putting %s (%d), %s'%(misc.ustr(uri), len(data), content_type)) - parent='/'.join(uri.split('/')[:-1]) - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - raise DAV_Forbidden - try: - node = self.uri2object(cr, uid, pool, uri2[:]) - except: - node = False - - if not node: - raise DAV_Forbidden - else: - try: - node.set_data(cr, uid, data) - except Exception, e: - import traceback - self.parent.log_error("Cannot save :%s", str(e)) - self.parent.log_message("Exc: %s", traceback.format_exc()) - raise DAV_Forbidden - - cr.commit() - cr.close() - return 201 - - def exists(self, uri): - """ test if a resource exists """ - result = False - cr, uid, pool, dbname, uri2 = self.get_cr(uri) - if not dbname: - cr.close() - return True - try: - node = self.uri2object(cr, uid, pool, uri2) - if node: - result = True - except: - pass - cr.close() - return result - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/caldav/caldav_node.py b/addons/caldav/caldav_node.py index fd17b52f478..8ec7422a045 100644 --- a/addons/caldav/caldav_node.py +++ b/addons/caldav/caldav_node.py @@ -25,31 +25,104 @@ import pooler import tools import time import base64 +from document import nodes +import StringIO + +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 + + 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 + +class node_calendar_collection(nodes.node_dir): + PROPS = { + "http://calendarserver.org/ns/" : ('getctag'), + } + M_NS = { + "http://calendarserver.org/ns/" : '_get_dav', + } -class node_calendar(object): - def __init__(self, path, context, calendar): - self.path = path - self.context = context - self.calendar_id = calendar.id - self.mimetype = 'ics' - self.create_date = calendar.create_date - self.write_date = calendar.write_date or calendar.create_date - self.content_length = 0 - self.displayname = calendar.name + def get_dav_props(self, cr): + return self.PROPS + - def get_data(self, cr, uid): - calendar_obj = pooler.get_pool(cr.dbname).get('basic.calendar') - return calendar_obj.export_cal(cr, uid, [self.calendar_id]) + def get_dav_eprop(self,cr, ns, propname): + if self.M_NS.has_key(ns): + prefix = self.M_NS[ns] + else: + print "No namespace:",ns, "( for prop:", propname,")" + return None - def get_data_len(self, cr): - return self.content_length + mname = prefix + "_" + propname - def set_data(self, cr, uid, data): - calendar_obj = pooler.get_pool(cr.dbname).get('basic.calendar') - return calendar_obj.import_cal(cr, uid, base64.encodestring(data), self.calendar_id) + if not hasattr(self, mname): + return None + try: + m = getattr(self, mname) + r = m(cr) + return r + except AttributeError, e: + print 'Property %s not supported' % propname + print "Exception:", e + return None + + 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: + res = name.split('.ics') + if len(res) > 1: + name = res[0] + ext = '.ics' + 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 = [] + for calender in fil_obj.browse(cr, uid, ids, context=ctx): + if not ext: + res.append(node_calendar(calender.name, self, self.context, calender)) + else: + res.append(res_node_calendar(name, self, self.context, calender)) + return res + + def _get_dav_owner(self, cr): + return False + + def get_etag(self, cr): """ Get a tag, unique per object + modification. @@ -64,24 +137,385 @@ class node_calendar(object): return str(wtime) def _get_ttag(self, cr): - return 'calendar-%d' % self.calendar_id + return 'calendar collection-%d' % self.dir_id + + def _get_dav_getctag(self, cr): + result = self.get_etag(cr) + return str(result) + + +class node_calendar(nodes.node_class): + our_type = 'collection' + PROPS = { + "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',)} + M_NS = { + "DAV:" : '_get_dav', + "http://calendarserver.org/ns/" : '_get_dav', + "urn:ietf:params:xml:ns:caldav" : '_get_caldav'} + + def __init__(self,path, parent, context, calendar): + super(node_calendar,self).__init__(path, parent,context) + self.calendar_id = calendar.id + self.mimetype = 'application/x-directory' + self.create_date = calendar.create_date + self.write_date = calendar.write_date or calendar.create_date + self.content_length = 0 + self.displayname = calendar.name + self.cal_type = calendar.type + + def _get_dav_getctag(self, cr): + result = self._get_ttag(cr) + ':' + str(time.time()) + return str(result) + + def match_dav_eprop(self, cr, match, ns, prop): + if ns == "DAV:" and prop == "getetag": + dirobj = self.context._dirobj + uid = self.context.uid + ctx = self.context.context.copy() + tem, dav_time = tuple(match.split(':')) + model, res_id = tuple(tem.split('_')) + model_obj = dirobj.pool.get(model) + model = model_obj.browse(cr, uid, res_id, context=ctx) + write_time = model.write_date or model.create_date + wtime = time.mktime(time.strptime(write_time,'%Y-%m-%d %H:%M:%S')) + if float(dav_time) == float(wtime): + return True + return False + res = super(node_calendar, self).match_dav_eprop(cr, match, ns, prop) + return res + + + def get_domain(self, cr, filters): + res = [] + dirobj = self.context._dirobj + uid = self.context.uid + ctx = self.context.context.copy() + ctx.update(self.dctx) + calendar_obj = dirobj.pool.get('basic.calendar') + if not filters: + return res + if filters.localName == 'calendar-query': + res = [] + for filter_child in filters.childNodes: + if filter_child.nodeType == filter_child.TEXT_NODE: + continue + if filter_child.localName == 'filter': + for vcalendar_filter in filter_child.childNodes: + if vcalendar_filter.nodeType == vcalendar_filter.TEXT_NODE: + continue + if vcalendar_filter.localName == 'comp-filter': + if vcalendar_filter.getAttribute('name') == 'VCALENDAR': + for vevent_filter in vcalendar_filter.childNodes: + 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')] + 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 + return res + + def children(self, cr, domain=None): + return self._child_get(cr, domain=domain) + + def child(self,cr, name, domain=None): + res = self._child_get(cr, name, domain=domain) + if res: + return res[0] + return None -class Calendar(osv.osv): - _inherit = 'basic.calendar' + 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 = [] + if name: + where.append(('id','=',int(name))) + if not domain: + domain = [] + #for opr1, opt, opr2 in domain: + # if opr1 == 'type' and opr2 != self.cal_type: + # return [] - def get_calendar_object(self, cr, uid, uri, context=None): - if not uri: + 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) + return res + + + + def get_dav_props(self, cr): + return self.PROPS + + def get_dav_eprop(self,cr, ns, propname): + if self.M_NS.has_key(ns): + prefix = self.M_NS[ns] + else: + print "No namespace:",ns, "( for prop:", propname,")" return None - if len(uri) > 1: + propname = propname.replace('-','_') + mname = prefix + "_" + propname + if not hasattr(self, mname): return None - name, file_type = tuple(uri[0].split('.')) - res = self.name_search(cr, uid, name) - if not res: - return None - calendar_id, calendar_name = res[0] - calendar = self.browse(cr, uid, calendar_id) - return node_calendar(uri, context, calendar) -Calendar() -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4 \ No newline at end of file + try: + m = getattr(self, mname) + r = m(cr) + return r + except AttributeError, e: + print 'Property %s not supported' % propname + print "Exception:", e + return None + + + def create_child(self,cr,path,data): + """ API function to create a child file object and node + Return the node_* created + """ + return self.set_data(cr, data) + + + def set_data(self, cr, data, fil_obj = None): + uid = self.context.uid + calendar_obj = self.context._dirobj.pool.get('basic.calendar') + return calendar_obj.import_cal(cr, uid, base64.encodestring(data), self.calendar_id) + + def get_data_len(self, cr, fil_obj = None): + return self.content_length + + + def _get_ttag(self,cr): + return 'calendar-%d' % (self.calendar_id,) + + + + def get_etag(self, cr): + """ 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) + + def _get_wtag(self, cr): + """ Return the modification time as a unique, compact string """ + if self.write_date: + wtime = time.mktime(time.strptime(self.write_date, '%Y-%m-%d %H:%M:%S')) + else: wtime = time.time() + return str(wtime) + + def rmcol(self, cr): + return False + + +class res_node_calendar(nodes.node_class): + our_type = 'file' + PROPS = { + "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',)} + M_NS = { + "http://calendarserver.org/ns/" : '_get_dav', + "urn:ietf:params:xml:ns:caldav" : '_get_caldav'} + + def __init__(self,path, parent, context, res_obj, res_model=None, res_id=None): + super(res_node_calendar,self).__init__(path, parent, context) + self.mimetype = 'text/calendar' + self.create_date = parent.create_date + self.write_date = parent.write_date or parent.create_date + 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 + self.create_date = res_obj.create_date + self.write_date = res_obj.write_date or res_obj.create_date + self.displayname = res_obj.name + + self.content_length = 0 + + self.model = res_model + self.res_id = res_id + + def open(self, cr, mode=False): + uid = self.context.uid + if self.type in ('collection','database'): + return False + s = StringIO.StringIO(self.get_data(cr)) + s.name = self + return s + + + + def get_dav_props(self, cr): + return self.PROPS + + def get_dav_eprop(self,cr, ns, propname): + if self.M_NS.has_key(ns): + prefix = self.M_NS[ns] + else: + print "No namespace:",ns, "( for prop:", propname,")" + return None + propname = propname.replace('-','_') + mname = prefix + "_" + propname + if not hasattr(self, mname): + return None + + try: + m = getattr(self, mname) + r = m(cr) + return r + except AttributeError, e: + print 'Property %s not supported' % propname + print "Exception:", e + return None + + + def get_data(self, cr, fil_obj = None): + uid = self.context.uid + calendar_obj = self.context._dirobj.pool.get('basic.calendar') + context = self.context.context.copy() + context.update({'model': self.model, 'res_id':self.res_id}) + res = calendar_obj.export_cal(cr, uid, [self.calendar_id], context=context) + return res + + def get_data_len(self, cr, fil_obj = None): + return self.content_length + + def set_data(self, cr, data, fil_obj = None): + uid = self.context.uid + calendar_obj = self.context._dirobj.pool.get('basic.calendar') + return calendar_obj.import_cal(cr, uid, base64.encodestring(data), self.calendar_id) + + def _get_ttag(self,cr): + res = False + if self.model and self.res_id: + res = '%s_%d' % (self.model, self.res_id) + elif self.calendar_id: + res = '%d' % (self.calendar_id) + return res + + + + def _get_caldav_calendar_data(self, cr): + return self.get_data(cr) + + + def _get_caldav_calendar_description(self, cr): + uid = self.context.uid + 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 + + + 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) + + 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_calendar_user_address_set(self, cr): + import xml.dom.minidom + dirobj = self.context._dirobj + 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:' + user.email) + href.appendChild(huri) + return href + + + 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.name, calendar.collection_id.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 rm(self, cr): + uid = self.context.uid + res = False + if self.type in ('collection','database'): + return False + if self.model and self.res_id: + document_obj = self.context._dirobj.pool.get(self.model) + if document_obj: + res = False + #res = document_obj.unlink(cr, uid, [self.res_id]) #TOFIX + + return res + + + + def _get_caldav_schedule_outbox_URL(self, cr): + return self._get_caldav_schedule_inbox_URL(cr) + + + def get_etag(self, cr): + """ 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) + + def _get_wtag(self, cr): + """ Return the modification time as a unique, compact string """ + if self.write_date: + wtime = time.mktime(time.strptime(self.write_date, '%Y-%m-%d %H:%M:%S')) + else: wtime = time.time() + return str(wtime) +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4 diff --git a/addons/caldav/caldav_view.xml b/addons/caldav/caldav_view.xml index a477eeb282d..4c1003023ae 100644 --- a/addons/caldav/caldav_view.xml +++ b/addons/caldav/caldav_view.xml @@ -1,61 +1,151 @@ - + + + Calendar Collections : Form + document.directory + form + + + + + + + + + + + Calendar Collections : Tree + document.directory + tree + + + + + + + + + - - Basic Calendar + + Calendar Collections + ir.actions.act_window + document.directory + form + tree,form + [('calendar_collection','=',True)] + + + + [('calendar_collection','=',False)] + + + + + tree + + + + + + form + + + + + + + + + + Calendar : Form basic.calendar form -
- - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + - - -
+ +
+
- - Calendar - ir.actions.act_window - basic.calendar - form - tree,form - - + + Calendar : Tree + basic.calendar + tree + + + + + + + + + + - + + Calendars + ir.actions.act_window + basic.calendar + form + tree,form + -
+ + + tree + + + + + + form + + + + + +
diff --git a/addons/caldav/calendar.py b/addons/caldav/calendar.py new file mode 100644 index 00000000000..3e4701a08d5 --- /dev/null +++ b/addons/caldav/calendar.py @@ -0,0 +1,1058 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-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 datetime import datetime, timedelta +from dateutil import parser +from dateutil.rrule import * +from osv import osv, fields +from tools.translate import _ +import base64 +import math +import pooler +import pytz +import re +import tools +import time +from caldav_node import res_node_calendar + +try: + import vobject +except ImportError: + raise osv.except_osv('vobject Import Error!','Please install python-vobject \ + from http://vobject.skyhouseconsulting.com/') + +# O-1 Optional and can come only once +# O-n Optional and can come more than once +# R-1 Required and can come only once +# R-n Required and can come more than once + +def uid2openobjectid(cr, uidval, oomodel, rdate): + """ UID To Open Object Id + @param cr: the current row, from the database cursor, + @param uidval: Get USerId vale + @oomodel: Open Object ModelName + @param rdate: Get Recurrent Date + """ + __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$') + wematch = __rege.match(uidval.encode('utf8')) + if not wematch: + return (False, None) + else: + model, id, dbname = wematch.groups() + model_obj = pooler.get_pool(cr.dbname).get(model) + if (not model == oomodel) or (not dbname == cr.dbname): + return (False, None) + qry = 'select distinct(id) from %s' % model_obj._table + if rdate: + qry += " where recurrent_id='%s'" % (rdate) + cr.execute(qry) + r_id = cr.fetchone() + if r_id: + return (id, r_id[0]) + cr.execute(qry) + ids = map(lambda x: str(x[0]), cr.fetchall()) + if id in ids: + return (id, None) + return (False, None) + +def openobjectid2uid(cr, uidval, oomodel): + """ Open Object Id To UId + @param cr: the current row, from the database cursor, + @param uidval: Get USerId vale + @oomodel: Open Object ModelName """ + + value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname) + return value + +def get_attribute_mapping(cr, uid, calname, context={}): + """ Attribute Mapping with Basic calendar fields and lines + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param calname: Get Calendar name + @param context: A standard dictionary for contextual values """ + + if not context: + context = {} + pool = pooler.get_pool(cr.dbname) + field_obj = pool.get('basic.calendar.fields') + type_obj = pool.get('basic.calendar.lines') + domain = [('object_id.model', '=', context.get('model'))] + if context.get('calendar_id'): + domain.append(('calendar_id', '=', context.get('calendar_id'))) + type_id = type_obj.search(cr, uid, domain) + fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])]) + res = {} + for field in field_obj.browse(cr, uid, fids): + attr = field.name.name + res[attr] = {} + res[attr]['field'] = field.field_id.name + res[attr]['type'] = field.field_id.ttype + if field.fn == 'hours': + res[attr]['type'] = "timedelta" + if res[attr]['type'] in ('one2many', 'many2many', 'many2one'): + res[attr]['object'] = field.field_id.relation + elif res[attr]['type'] in ('selection') and field.mapping: + res[attr]['mapping'] = eval(field.mapping) + if not res.get('uid', None): + res['uid'] = {} + res['uid']['field'] = 'id' + res['uid']['type'] = "integer" + return res + +def map_data(cr, uid, obj): + """ Map Data + @param self: The object pointer + @param cr: the current row, from the database cursor,""" + + vals = {} + for map_dict in obj.__attribute__: + map_val = obj.ical_get(map_dict, 'value') + field = obj.ical_get(map_dict, 'field') + field_type = obj.ical_get(map_dict, 'type') + if field: + if field_type == 'selection': + if not map_val: + continue + mapping = obj.__attribute__[map_dict].get('mapping', False) + if mapping: + map_val = mapping[map_val.lower()] + else: + map_val = map_val.lower() + if field_type == 'many2many': + ids = [] + if not map_val: + vals[field] = ids + continue + model = obj.__attribute__[map_dict].get('object', False) + modobj = obj.pool.get(model) + for map_vall in map_val: + id = modobj.create(cr, uid, map_vall) + ids.append(id) + vals[field] = [(6, 0, ids)] + continue + if field_type == 'many2one': + id = None + if not map_val or not isinstance(map_val, dict): + vals[field] = id + continue + model = obj.__attribute__[map_dict].get('object', False) + modobj = obj.pool.get(model) + id = modobj.create(cr, uid, map_val) + vals[field] = id + continue + if field_type == 'timedelta': + if map_val: + vals[field] = (map_val.seconds/float(86400) + map_val.days) + vals[field] = map_val + return vals + +class CalDAV(object): + __attribute__ = {} + + + + def ical_set(self, name, value, type): + """ set calendar Attribute + @param self: The object pointer, + @param name: Get Attribute Name + @param value: Get Attribute Value + @param type: Get Attribute Type + """ + if name in self.__attribute__ and self.__attribute__[name]: + self.__attribute__[name][type] = value + return True + + def ical_get(self, name, type): + """ Get calendar Attribute + @param self: The object pointer, + @param name: Get Attribute Name + @param type: Get Attribute Type + """ + + if self.__attribute__.get(name): + val = self.__attribute__.get(name).get(type, None) + valtype = self.__attribute__.get(name).get('type', None) + if type == 'value': + if valtype and valtype == 'datetime' and val: + if isinstance(val, list): + val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val)) + else: + val = val.strftime('%Y-%m-%d %H:%M:%S') + return val + else: + return self.__attribute__.get(name, None) + + def ical_reset(self, type): + """ Reset Calendar Attribute + @param self: The object pointer, + @param type: Get Attribute Type + """ + + for name in self.__attribute__: + if self.__attribute__[name]: + self.__attribute__[name][type] = None + return True + + def parse_ics(self, cr, uid, child, cal_children=None, context=None): + """ parse calendaring and scheduling information + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values """ + + att_data = [] + for cal_data in child.getChildren(): + if cal_data.name.lower() == 'attendee': + ctx = context.copy() + if cal_children: + ctx.update({'model': cal_children[cal_data.name.lower()]}) + attendee = self.pool.get('basic.calendar.attendee') + att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx)) + self.ical_set(cal_data.name.lower(), att_data, 'value') + continue + if cal_data.name.lower() == 'valarm': + alarm = self.pool.get('basic.calendar.alarm') + ctx = context.copy() + if cal_children: + ctx.update({'model': cal_children[cal_data.name.lower()]}) + vals = alarm.import_cal(cr, uid, cal_data, context=ctx) + self.ical_set(cal_data.name.lower(), vals, 'value') + continue + if cal_data.name.lower() == 'exdate': + exval = map(lambda x: str(x), cal_data.value) + self.ical_set(cal_data.name.lower(), ','.join(exval), 'value') + continue + 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') + self.ical_set(cal_data.name.lower(), cal_data.value, 'value') + vals = map_data(cr, uid, self) + return vals + + def create_ics(self, cr, uid, datas, name, ical, context=None): + """ create calendaring and scheduling information + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values """ + + if not datas: + return + timezones = [] + for data in datas: + tzval = None + vevent = ical.add(name) + for field in self.__attribute__.keys(): + map_field = self.ical_get(field, 'field') + map_type = self.ical_get(field, 'type') + if map_field in data.keys(): + if field == 'uid': + model = context.get('model', None) + if not model: + continue + uidval = openobjectid2uid(cr, data[map_field], model) + model_obj = self.pool.get(model) + r_ids = [] + if model_obj._columns.get('recurrent_uid', None): + cr.execute('select id from %s where recurrent_uid=%s' + % (model_obj._table, data[map_field])) + r_ids = map(lambda x: x[0], cr.fetchall()) + if r_ids: + rdata = self.pool.get(model).read(cr, uid, r_ids) + event_obj = self.pool.get('basic.calendar.event') + rcal = event_obj.export_cal(cr, uid, rdata, context=context) + for revents in rcal.contents['vevent']: + ical.contents['vevent'].append(revents) + if data.get('recurrent_uid', None): + uidval = openobjectid2uid(cr, data['recurrent_uid'], model) + vevent.add('uid').value = uidval + elif field == 'attendee' and data[map_field]: + model = self.__attribute__[field].get('object', False) + attendee_obj = self.pool.get('basic.calendar.attendee') + vevent = attendee_obj.export_cal(cr, uid, model, \ + data[map_field], vevent, context=context) + elif field == 'valarm' and data[map_field]: + model = self.__attribute__[field].get('object', False) + ctx = context.copy() + ctx.update({'model': model}) + alarm_obj = self.pool.get('basic.calendar.alarm') + vevent = alarm_obj.export_cal(cr, uid, model, \ + data[map_field][0], vevent, context=ctx) + elif field == 'vtimezone' and data[map_field] and data[map_field] not in timezones: + tzval = data[map_field] + tz_obj = self.pool.get('basic.calendar.timezone') + ical = tz_obj.export_cal(cr, uid, None, \ + data[map_field], ical, context=context) + timezones.append(data[map_field]) + elif data[map_field]: + if map_type in ("char", "text"): + if field in ('exdate'): + vevent.add(field).value = map(parser.parse, (data[map_field]).split(',')) + else: + vevent.add(field).value = tools.ustr(data[map_field]) + elif map_type in ('datetime', 'date') and data[map_field]: + dtfield = vevent.add(field) + dtfield.value = parser.parse(data[map_field]) + if tzval: + dtfield.params['TZID'] = [tzval.title()] + elif map_type == "timedelta": + vevent.add(field).value = timedelta(hours=data[map_field]) + elif map_type == "many2one": + vevent.add(field).value = tools.ustr(data.get(map_field)[1]) + elif map_type in ("float", "integer"): + vevent.add(field).value = str(data.get(map_field)) + elif map_type == "selection": + if not self.ical_get(field, 'mapping'): + vevent.add(field).value = (tools.ustr(data[map_field])).upper() + else: + for key1, val1 in self.ical_get(field, 'mapping').items(): + if val1 == data[map_field]: + vevent.add(field).value = key1 + return vevent + + def check_import(self, cr, uid, vals, context={}): + """ + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param vals: Get Values + @param context: A standard dictionary for contextual values + """ + + ids = [] + model_obj = self.pool.get(context.get('model')) + recur_pool = {} + try: + for val in vals: + exists, r_id = uid2openobjectid(cr, val['id'], context.get('model'), \ + val.get('recurrent_id')) + if val.has_key('create_date'): val.pop('create_date') + u_id = val.get('id', None) + val.pop('id') + if exists and r_id: + val.update({'recurrent_uid': exists}) + model_obj.write(cr, uid, [r_id], val) + ids.append(r_id) + elif exists: + model_obj.write(cr, uid, [exists], val) + ids.append(exists) + else: + if u_id in recur_pool and val.get('recurrent_id'): + val.update({'recurrent_uid': recur_pool[u_id]}) + revent_id = model_obj.create(cr, uid, val) + ids.append(revent_id) + else: + event_id = model_obj.create(cr, uid, val) + recur_pool[u_id] = event_id + ids.append(event_id) + except Exception, e: + raise osv.except_osv(('Error !'), (str(e))) + return ids + + def export_cal(self, cr, uid, datas, vobj=None, context={}): + """ Export Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param datas: Get Data's for caldav + @param context: A standard dictionary for contextual values + """ + + try: + self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context) + ical = vobject.iCalendar() + self.create_ics(cr, uid, datas, vobj, ical, context=context) + return ical + except Exception, e: + raise osv.except_osv(('Error !'), (str(e))) + + def import_cal(self, cr, uid, content, data_id=None, context=None): + """ Import Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param data_id: Get Data’s ID or False + @param context: A standard dictionary for contextual values + """ + + ical_data = base64.decodestring(content) + self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context) + parsedCal = vobject.readOne(ical_data) + res = [] + vals = {} + for child in parsedCal.getChildren(): + if child.name.lower() in ('vevent', 'vtodo'): + vals = self.parse_ics(cr, uid, child, context=context) + else: + vals = {} + continue + if vals: res.append(vals) + self.ical_reset('value') + return res + +class Calendar(CalDAV, osv.osv): + _name = 'basic.calendar' + _calname = 'calendar' + + __attribute__ = { + 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object. + 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number + # or the minimum and maximum range of the iCalendar specification + # that is required in order to interpret the iCalendar object. + 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object. + 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object. + 'vevent': None, # Use: O-n, Type: Collection of Event class + 'vtodo': None, # Use: O-n, Type: Collection of ToDo class + 'vjournal': None, # Use: O-n, Type: Collection of Journal class + 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class + 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class + } + _columns = { + 'name': fields.char("Name", size=64), + 'user_id': fields.many2one('res.users', 'Owner'), + 'collection_id': fields.many2one('document.directory', 'Collection', \ + required=True), + 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \ + string="Type", size=64), + 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'), + 'create_date': fields.datetime('Created Date'), + 'write_date': fields.datetime('Modifided Date'), + } + + def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None): + if not context: + context = {} + if not domain: + domain = [] + res = [] + 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) + line_domain += domain + if ctx_res_id: + line_domain += [('id','=',ctx_res_id)] + mod_obj = self.pool.get(line.object_id.model) + data_ids = mod_obj.search(cr, uid, line_domain, context=context) + for data in mod_obj.browse(cr, uid, data_ids, context): + ctx = parent and parent.context or None + node = res_node_calendar('%s' %data.id, parent, ctx, data, line.object_id.model, data.id) + res.append(node) + return res + + def export_cal(self, cr, uid, ids, vobj='vevent', context=None): + """ Export Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of calendar’s IDs + @param context: A standard dictionary for contextual values + """ + if not context: + context = {} + ctx_model = context.get('model', None) + ctx_res_id = context.get('res_id', None) + ical = vobject.iCalendar() + 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 + domain = eval(line.domain) + if ctx_res_id: + domain += [('id','=',ctx_res_id)] + mod_obj = self.pool.get(line.object_id.model) + data_ids = mod_obj.search(cr, uid, domain, context=context) + datas = mod_obj.read(cr, uid, data_ids, context=context) + context.update({'model': line.object_id.model, + 'calendar_id': cal.id + }) + self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context) + self.create_ics(cr, uid, datas, line.name, ical, context=context) + return ical.serialize() + + def import_cal(self, cr, uid, content, data_id=None, context=None): + """ Import Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param data_id: Get Data’s ID or False + @param context: A standard dictionary for contextual values + """ + + if not context: + context = {} + vals = [] + ical_data = base64.decodestring(content) + parsedCal = vobject.readOne(ical_data) + if not data_id: + data_id = self.search(cr, uid, [])[0] + cal = self.browse(cr, uid, data_id, context=context) + cal_children = {} + count = 0 + for line in cal.line_ids: + cal_children[line.name] = line.object_id.model + for child in parsedCal.getChildren(): + if child.name.lower() in cal_children: + context.update({'model': cal_children[child.name.lower()], + 'calendar_id': cal['id'] + }) + self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context) + val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context) + vals.append(val) + obj = self.pool.get(cal_children[child.name.lower()]) + if hasattr(obj, 'check_import'): + obj.check_import(cr, uid, vals, context=context) + else: + self.check_import(cr, uid, vals, context=context) + return {} +Calendar() + + +class basic_calendar_line(osv.osv): + """ Calendar Lines """ + + _name = 'basic.calendar.lines' + _description = 'Calendar Lines' + + _columns = { + 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \ + ('valarm', 'Alarm'), \ + ('attendee', 'Attendee')], \ + string="Type", size=64), + 'object_id': fields.many2one('ir.model', 'Object'), + 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \ + required=True, ondelete='cascade'), + 'domain': fields.char('Domain', size=124), + 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping') + } + + _defaults = { + 'domain': lambda *a: '[]', + } + + def create(self, cr, uid, vals, context={}): + """ create calendar's line + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param vals: Get the Values + @param context: A standard dictionary for contextual values + """ + + cr.execute("Select count(id) from basic_calendar_lines \ + where name='%s' and calendar_id=%s" % (vals.get('name'), vals.get('calendar_id'))) + res = cr.fetchone() + if res: + if res[0] > 0: + raise osv.except_osv(_('Warning !'), _('Can not create \ +line "%s" more than once' % (vals.get('name')))) + return super(basic_calendar_line, self).create(cr, uid, vals, context=context) + +basic_calendar_line() + + +class basic_calendar_attribute(osv.osv): + _name = 'basic.calendar.attributes' + _description = 'Calendar attributes' + _columns = { + 'name': fields.char("Name", size=64, required=True), + 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \ + ('alarm', 'Alarm'), \ + ('attendee', 'Attendee')], \ + string="Type", size=64, required=True), + } + +basic_calendar_attribute() + + +class basic_calendar_fields(osv.osv): + """ Calendar fields """ + + _name = 'basic.calendar.fields' + _description = 'Calendar fields' + + _columns = { + 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'), + 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True), + 'type_id': fields.many2one('basic.calendar.lines', 'Type', \ + required=True, ondelete='cascade'), + 'expr': fields.char("Expression", size=64), + 'fn': fields.selection([('field', 'Use the field'), + ('const', 'Expression as constant'), + ('hours', 'Interval in hours'), + ], 'Function'), + 'mapping': fields.text('Mapping'), + } + + _defaults = { + 'fn': lambda *a: 'field', + } + + def check_line(self, cr, uid, vals, name, context=None): + """ check calendar's line + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param vals: Get Values + @param context: A standard dictionary for contextual values + """ + f_obj = self.pool.get('ir.model.fields') + field = f_obj.browse(cr, uid, vals['field_id'], context=context) + relation = field.relation + line_obj = self.pool.get('basic.calendar.lines') + l_id = line_obj.search(cr, uid, [('name', '=', name)]) + if l_id: + line = line_obj.browse(cr, uid, l_id, context=context)[0] + line_rel = line.object_id.model + if (relation != 'NULL') and (not relation == line_rel): + raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name))) + return True + + def create(self, cr, uid, vals, context={}): + """ Create Calendar's fields + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param vals: Get Values + @param context: A standard dictionary for contextual values + """ + + cr.execute('select name from basic_calendar_attributes \ + where id=%s' % (vals.get('name'))) + name = cr.fetchone() + name = name[0] + if name in ('valarm', 'attendee'): + self.check_line(cr, uid, vals, name, context=context) + cr.execute("Select count(id) from basic_calendar_fields \ + where name=%s and type_id=%s" % (vals.get('name'), vals.get('type_id'))) + res = cr.fetchone() + if res: + if res[0] > 0: + raise osv.except_osv(_('Warning !'), _('Can not map the field more than once')) + return super(basic_calendar_fields, self).create(cr, uid, vals, context=context) + + def write(self, cr, uid, ids, vals, context=None): + """ write Calendar's fields + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param vals: Get Values + @param context: A standard dictionary for contextual values + """ + + if not vals: + return + for id in ids: + field = self.browse(cr, uid, id, context=context) + name = field.name.name + if name in ('valarm', 'attendee'): + self.check_line(cr, uid, vals, name, context=context) + qry = "Select count(id) from basic_calendar_fields \ + where name=%s and type_id=%s" % (field.name.id, field.type_id.id) + cr.execute(qry) + res = cr.fetchone() + if res: + if res[0] > 1: + raise osv.except_osv(_('Warning !'), _('Can not map same field more than once')) + return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context) + +basic_calendar_fields() + + +class Event(CalDAV, osv.osv_memory): + _name = 'basic.calendar.event' + _calname = 'vevent' + __attribute__ = { + 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL" + 'created': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the calendar information was created by the calendar user agent in the calendar store. + 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. + 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins. + 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component. + 'last-mod': None, # Use: O-1, Type: DATE-TIME Specifies the date and time that the information associated with the calendar component was last revised in the calendar store. + 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component. + 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component. + 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component. + 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created. + 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision. + 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component. + 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component. + 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches. + 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component. + 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object. + 'recurid': None, + 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component. + 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component. + 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component. + 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user. + 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component. + 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component. + 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set. + 'rstatus': None, + 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component. + # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M + 'resources': None, # Use: O-n, Type: TEXT, Defines the equipment or resources anticipated for an activity specified by a calendar entity like RESOURCES:EASEL,PROJECTOR,VCR, LANGUAGE=fr:1 raton-laveur + 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set. + 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions. + 'x-prop': None, + 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time. + 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends. + } + + def export_cal(self, cr, uid, datas, vobj='vevent', context={}): + """ Export calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param datas: Get datas + @param context: A standard dictionary for contextual values + """ + + return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context) + +Event() + + +class ToDo(CalDAV, osv.osv_memory): + _name = 'basic.calendar.todo' + _calname = 'vtodo' + + __attribute__ = { + 'class': None, + 'completed': None, + 'created': None, + 'description': None, + 'dtstamp': None, + 'dtstart': None, + 'duration': None, + 'due': None, + 'geo': None, + 'last-mod ': None, + 'location': None, + 'organizer': None, + 'percent': None, + 'priority': None, + 'recurid': None, + 'seq': None, + 'status': None, + 'summary': None, + 'uid': None, + 'url': None, + 'attach': None, + 'attendee': None, + 'categories': None, + 'comment': None, + 'contact': None, + 'exdate': None, + 'exrule': None, + 'rstatus': None, + 'related': None, + 'resources': None, + 'rdate': None, + 'rrule': None, + } + + def export_cal(self, cr, uid, datas, vobj='vevent', context={}): + """ Export Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param datas: Get datas + @param context: A standard dictionary for contextual values + """ + + return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context) + +ToDo() + + +class Journal(CalDAV): + __attribute__ = { + } + + +class FreeBusy(CalDAV): + __attribute__ = { + 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component. + 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins. + 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends. + 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time. + 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created. + 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component. + 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component. + 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object. + 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component. + 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user. + 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals. + 'rstatus': None, + 'X-prop': None, + } + + +class Timezone(CalDAV, osv.osv_memory): + _name = 'basic.calendar.timezone' + _calname = 'vtimezone' + + __attribute__ = { + 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component. + 'last-mod': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the information associated with the calendar component was last revised in the calendar store. + 'tzurl': None, # Use: O-1, Type: URI, Provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to-date version of itself. + 'standardc': {'tzprop': None}, # Use: R-1, + 'daylightc': {'tzprop': None}, # Use: R-1, + 'x-prop': None, # Use: O-n, Type: Text, + } + + def get_name_offset(self, cr, uid, tzid, context={}): + """ Get Name Offset value + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + + mytz = pytz.timezone(tzid.title()) + mydt = datetime.now(tz=mytz) + offset = mydt.utcoffset() + val = offset.days * 24 + float(offset.seconds) / 3600 + realoffset = '%02d%02d' % (math.floor(abs(val)), \ + round(abs(val) % 1 + 0.01, 2) * 60) + realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset)) + return (mydt.tzname(), realoffset) + + def export_cal(self, cr, uid, model, tzid, ical, context={}): + """ Export Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param model: Get Model's name + @param context: A standard dictionary for contextual values + """ + + ctx = context.copy() + ctx.update({'model': model}) + cal_tz = ical.add('vtimezone') + cal_tz.add('TZID').value = tzid.title() + tz_std = cal_tz.add('STANDARD') + tzname, offset = self.get_name_offset(cr, uid, tzid) + tz_std.add("TZOFFSETFROM").value = offset + tz_std.add("TZOFFSETTO").value = offset + tz_std.add("DTSTART").value = datetime.now() # TODO + tz_std.add("TZNAME").value = tzname + return ical + + def import_cal(self, cr, uid, ical_data, context=None): + """ Import Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ical_data: Get calendar's data + @param context: A standard dictionary for contextual values + """ + + for child in ical_data.getChildren(): + if child.name.lower() == 'tzid': + tzname = child.value + self.ical_set(child.name.lower(), tzname, 'value') + vals = map_data(cr, uid, self) + return vals + +Timezone() + + +class Alarm(CalDAV, osv.osv_memory): + _name = 'basic.calendar.alarm' + _calname = 'alarm' + + __attribute__ = { + 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE" + 'description': None, # Type: Text, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. Use:- R-1 for DISPLAY,Use:- R-1 for EMAIL,Use:- R-1 for PROCEDURE + 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL + 'attendee': None, # Use: R-n, Type: CAL-ADDRESS, Contain the email address of attendees to receive the message. It can also include one or more. Use for EMAIL + 'trigger': None, # Use: R-1, Type: DURATION, The "TRIGGER" property specifies a duration prior to the start of an event or a to-do. The "TRIGGER" edge may be explicitly set to be relative to the "START" or "END" of the event or to-do with the "related" parameter of the "TRIGGER" property. The "TRIGGER" property value type can alternatively be set to an absolute calendar date and time of day value. Use for all action like AUDIO, DISPLAY, EMAIL and PROCEDURE + 'duration': None, # Type: DURATION, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY + 'repeat': None, # Type: INTEGER, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY + 'attach': None, # Use:- O-n: which MUST point to a sound resource, which is rendered when the alarm is triggered for AUDIO, Use:- O-n: which are intended to be sent as message attachments for EMAIL, Use:- R-1:which MUST point to a procedure resource, which is invoked when the alarm is triggered for PROCEDURE. + 'x-prop': None, + } + + def export_cal(self, cr, uid, model, alarm_id, vevent, context={}): + """ Export Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param model: Get Model's name + @param alarm_id: Get Alarm's Id + @param context: A standard dictionary for contextual values + """ + + valarm = vevent.add('valarm') + alarm_object = self.pool.get(model) + alarm_data = alarm_object.read(cr, uid, alarm_id, []) + + # Compute trigger data + interval = alarm_data['trigger_interval'] + occurs = alarm_data['trigger_occurs'] + duration = (occurs == 'after' and alarm_data['trigger_duration']) \ + or -(alarm_data['trigger_duration']) + related = alarm_data['trigger_related'] + trigger = valarm.add('TRIGGER') + trigger.params['related'] = [related.upper()] + if interval == 'days': + delta = timedelta(days=duration) + if interval == 'hours': + delta = timedelta(hours=duration) + if interval == 'minutes': + delta = timedelta(minutes=duration) + trigger.value = delta + + # Compute other details + valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP' + valarm.add('ACTION').value = alarm_data['action'] + return vevent + + def import_cal(self, cr, uid, ical_data, context=None): + """ Import Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ical_data: Get calendar's Data + @param context: A standard dictionary for contextual values + """ + + ctx = context.copy() + ctx.update({'model': context.get('model', None)}) + self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) + for child in ical_data.getChildren(): + if child.name.lower() == 'trigger': + seconds = child.value.seconds + days = child.value.days + diff = (days * 86400) + seconds + interval = 'days' + related = 'before' + if not seconds: + duration = abs(days) + related = days > 0 and 'after' or 'before' + elif (abs(diff) / 3600) == 0: + duration = abs(diff / 60) + interval = 'minutes' + related = days >= 0 and 'after' or 'before' + else: + duration = abs(diff / 3600) + interval = 'hours' + related = days >= 0 and 'after' or 'before' + self.ical_set('trigger_interval', interval, 'value') + self.ical_set('trigger_duration', duration, 'value') + self.ical_set('trigger_occurs', related.lower(), 'value') + if child.params: + if child.params.get('related'): + self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value') + else: + self.ical_set(child.name.lower(), child.value.lower(), 'value') + vals = map_data(cr, uid, self) + return vals + +Alarm() + + +class Attendee(CalDAV, osv.osv_memory): + _name = 'basic.calendar.attendee' + _calname = 'attendee' + + __attribute__ = { + 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN". + 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property. + 'role': None, # Use: 0-1 Specify the participation role for the calendar user specified by the property like "CHAIR"/"REQ-PARTICIPANT"/"OPT-PARTICIPANT"/"NON-PARTICIPANT" + 'partstat': None, # Use: 0-1 Specify the participation status for the calendar user specified by the property. like use for VEVENT:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED", use for VTODO:-"NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED"/"COMPLETED"/"IN-PROCESS" and use for VJOURNAL:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED". + 'rsvp': None, # Use: 0-1 Specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value like TRUE / FALSE. + 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation. + 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property. + 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property. + 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property. + 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property. + 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter. + } + + def import_cal(self, cr, uid, ical_data, context=None): + """ Import Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ical_data: Get calendar's Data + @param context: A standard dictionary for contextual values + """ + + ctx = context.copy() + ctx.update({'model': context.get('model', None)}) + self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) + for para in ical_data.params: + if para.lower() == 'cn': + self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \ + ical_data.value, 'value') + else: + self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value') + if not ical_data.params.get('CN'): + self.ical_set('cn', ical_data.value, 'value') + vals = map_data(cr, uid, self) + return vals + + def export_cal(self, cr, uid, model, attendee_ids, vevent, context={}): + """ Export Calendar + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param model: Get model's name + @param attendee_ids: Get Attendee's Id + @param context: A standard dictionary for contextual values + """ + + attendee_object = self.pool.get(model) + ctx = context.copy() + ctx.update({'model': model}) + self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx) + for attendee in attendee_object.read(cr, uid, attendee_ids, []): + attendee_add = vevent.add('attendee') + cn_val = '' + for a_key, a_val in self.__attribute__.items(): + if attendee[a_val['field']] and a_val['field'] != 'cn': + if a_val['type'] in ('text', 'char', 'selection'): + attendee_add.params[a_key] = [str(attendee[a_val['field']])] + elif a_val['type'] == 'boolean': + attendee_add.params[a_key] = [str(attendee[a_val['field']])] + if a_val['field'] == 'cn' and attendee[a_val['field']]: + cn_val = [str(attendee[a_val['field']])] + if cn_val: + attendee_add.params['CN'] = cn_val + if not attendee['email']: + raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id')) + elif attendee['email']: + attendee_add.value = 'MAILTO:' + attendee['email'] + return vevent + +Attendee() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/caldav/calendar_collection.py b/addons/caldav/calendar_collection.py new file mode 100644 index 00000000000..06e799b1686 --- /dev/null +++ b/addons/caldav/calendar_collection.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-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 +from tools.translate import _ +import caldav_node + +class calendar_collection(osv.osv): + _inherit = 'document.directory' + _columns = { + 'calendar_collection' : fields.boolean('Calendar Collection'), + 'calendar_ids': fields.one2many('basic.calendar', 'collection_id', 'Calendars'), + } + _default = { + 'calendar_collection' : False, + } + def _get_root_calendar_directory(self, cr, uid, context=None): + objid = self.pool.get('ir.model.data') + try: + mid = objid._get_id(cr, uid, 'document', 'dir_calendars') + if not mid: + return False + 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)) + 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_description(self, cr, uid, ids, context=None): + #TODO : return description of all calendars + if not context: + context = {} + return False + + def get_schedule_inbox_URL(self, cr, uid, ids, context=None): + calendar_obj = self.pool.get('basic.calendar') + + calendar_ids = calendar_obj.search(cr, uid, [ + ('user_id', '=', uid), ('collection_id', 'in', ids) + ], limit=1, context=context) + + root_cal_dir = self._get_root_calendar_directory(cr, uid, context=context) + if not calendar_ids: + return root_cal_dir + calendar_id = calendar_ids[0] + calendar = calendar_obj.browse(cr, uid, calendar_id, + context=context) + return '%s/%s' %(root_cal_dir, calendar.name) + + def get_schedule_outbox_URL(self, cr, uid, ids, context=None): + return self.get_schedule_inbox_URL(cr, uid, ids, context=context) + +calendar_collection() diff --git a/addons/caldav/security/ir.model.access.csv b/addons/caldav/security/ir.model.access.csv index 74064f43d75..4dd04fa4c68 100644 --- a/addons/caldav/security/ir.model.access.csv +++ b/addons/caldav/security/ir.model.access.csv @@ -1,5 +1,4 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"access_basic_calendar_all","basic.calendar","model_basic_calendar","base.group_user",1,1,1,1 "access_basic_calendar_event_all","basic.calendar.event","model_basic_calendar_event","base.group_user",1,1,1,0 "access_basic_calendar_attendee_all","basic.calendar.attendee","model_basic_calendar_attendee","base.group_user",1,1,1,0 "access_calendar_todo_all","basic.calendar.todo","model_basic_calendar_todo","base.group_user",1,1,1,0 diff --git a/addons/caldav/webdav_server.py b/addons/caldav/webdav_server.py deleted file mode 100644 index 40519ee19a2..00000000000 --- a/addons/caldav/webdav_server.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- encoding: utf-8 -*- -# -# Copyright P. Christeas 2008,2009 -# -# WARNING: This program as such is intended to be used by professional -# programmers who take the whole responsability of assessing all potential -# consequences resulting from its eventual inadequacies and bugs -# End users who are looking for a ready-to-use solution with commercial -# garantees and support are strongly adviced to contract a Free Software -# Service Company -# -# This program is Free Software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# 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 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. -############################################################################### - - -import netsvc -from caldav_fs import tinydav_handler -from tools.config import config -from DAV.WebDAVServer import DAVRequestHandler -from service.websrv_lib import HTTPDir,FixSendError - - -class DAVHandler(FixSendError, DAVRequestHandler): - verbose = False - - def get_userinfo(self, user, pw): - return False - - def _log(self, message): - netsvc.Logger().notifyChannel("webdav", netsvc.LOG_DEBUG, message) - - def handle(self): - pass - - def finish(self): - pass - - def setup(self): - davpath = '/calendar/' - self.baseuri = "http://%s:%d%s"% (self.server.server_name, self.server.server_port, davpath) - self.IFACE_CLASS = tinydav_handler(self) - pass - - def log_message(self, format, *args): - netsvc.Logger().notifyChannel('webdav', netsvc.LOG_DEBUG_RPC, format % args) - - def log_error(self, format, *args): - netsvc.Logger().notifyChannel('webdav', netsvc.LOG_WARNING, format % args) - - -try: - from service.http_server import reg_http_service, OpenERPAuthProvider - davpath = '/calendar/' - handler = DAVHandler - handler.verbose = config.get_misc('webdav', 'verbose', True) - handler.debug = config.get_misc('webdav', 'debug', True) - reg_http_service(HTTPDir(davpath, DAVHandler, OpenERPAuthProvider())) - netsvc.Logger().notifyChannel('webdav', netsvc.LOG_INFO, "WebDAV service registered at path: %s/ "% davpath) -except Exception, e: - logger = netsvc.Logger() - logger.notifyChannel('webdav', netsvc.LOG_ERROR, 'Cannot launch webdav: %s' % e) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4 - - - diff --git a/addons/caldav/wizard/calendar_event_import_view.xml b/addons/caldav/wizard/calendar_event_import_view.xml index 6a697de5fee..7e31d08c1e9 100644 --- a/addons/caldav/wizard/calendar_event_import_view.xml +++ b/addons/caldav/wizard/calendar_event_import_view.xml @@ -46,7 +46,7 @@ new
- diff --git a/addons/caldav/wizard/calendar_event_subscribe_view.xml b/addons/caldav/wizard/calendar_event_subscribe_view.xml index 43224013268..39eaae28540 100644 --- a/addons/caldav/wizard/calendar_event_subscribe_view.xml +++ b/addons/caldav/wizard/calendar_event_subscribe_view.xml @@ -52,4 +52,4 @@ view_mode="form" target="new" view_type="form" /> - \ No newline at end of file + diff --git a/addons/crm/__init__.py b/addons/crm/__init__.py index 1c0cc81dd38..de0fac56e29 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -24,13 +24,12 @@ import crm_mailgate import crm_action_rule import crm_segmentation import crm_meeting -import crm_opportunity import crm_lead -import crm_phonecall -import crm_claim -import crm_fundraising -import crm_helpdesk +import crm_opportunity +import crm_phonecall + +import crm_installer import report import wizard diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index f9ec0e7d315..bc1dc0a7c4e 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -21,12 +21,12 @@ { - 'name': 'Customer & Supplier Relationship Management', - 'version': '1.0', - 'category': 'Generic Modules/CRM & SRM', + 'name': 'Customer & Supplier Relationship Management', + 'version': '1.0', + 'category': 'Generic Modules/CRM & SRM', 'description': """The generic Open ERP Customer Relationship Management system enables a group of people to intelligently and efficiently manage -leads, opportunities, claims, meeting, phonecall etc. +leads, opportunities, meeting, phonecall etc. It manages key tasks such as communication, identification, prioritization, assignment, resolution and notification. @@ -41,86 +41,78 @@ appropriate staff, and making sure all future correspondence gets to the right place. The CRM module has a email gateway for the synchronisation interface -between mails and Open ERP.""", - 'author': 'Tiny', - 'website': 'http://www.openerp.com', +between mails and Open ERP.""", + 'author': 'Tiny', + 'website': 'http://www.openerp.com', 'depends': [ 'base', - 'base_action_rule', - 'process', - 'mail_gateway', - 'base_calendar', - 'resource', - ], + 'base_action_rule', + 'process', + 'mail_gateway', + 'base_calendar', + 'resource', + ], 'init_xml': [ - 'crm_data.xml', - 'crm_meeting_data.xml', - 'crm_claims_data.xml', - 'crm_fund_data.xml', - 'crm_helpdesk_data.xml', - 'crm_lead_data.xml', - 'crm_meeting_data.xml', - 'crm_opportunity_data.xml', - 'crm_phonecall_data.xml', - ], + 'crm_data.xml', + 'crm_meeting_data.xml', + 'crm_lead_data.xml', + 'crm_meeting_data.xml', + 'crm_opportunity_data.xml', + 'crm_phonecall_data.xml', + ], 'update_xml': [ - 'wizard/crm_lead_to_partner_view.xml', - 'wizard/crm_lead_to_opportunity_view.xml', + 'wizard/crm_lead_to_partner_view.xml', + 'wizard/crm_lead_to_opportunity_view.xml', - 'wizard/crm_phonecall_to_phonecall_view.xml', - 'wizard/crm_phonecall_to_partner_view.xml', - 'wizard/crm_phonecall_to_opportunity_view.xml', + 'wizard/crm_phonecall_to_phonecall_view.xml', + 'wizard/crm_phonecall_to_partner_view.xml', + 'wizard/crm_phonecall_to_opportunity_view.xml', - 'wizard/crm_opportunity_to_phonecall_view.xml', - 'wizard/crm_partner_to_opportunity_view.xml', + 'wizard/crm_opportunity_to_phonecall_view.xml', + 'wizard/crm_partner_to_opportunity_view.xml', - 'wizard/crm_forward_to_partner_view.xml', - 'wizard/crm_send_email_view.xml', - 'wizard/crm_email_add_cc_view.xml', - 'crm_view.xml', + 'wizard/crm_forward_to_partner_view.xml', + 'wizard/crm_send_email_view.xml', + 'crm_view.xml', - 'crm_action_rule_view.xml', - 'crm_lead_view.xml', - 'crm_lead_menu.xml', - 'crm_meeting_view.xml', - 'crm_meeting_menu.xml', - 'crm_phonecall_view.xml', - 'crm_phonecall_menu.xml', - 'crm_opportunity_view.xml', - 'crm_opportunity_menu.xml', - 'crm_fund_view.xml', - 'crm_fund_menu.xml', - 'crm_claims_view.xml', - 'crm_claims_menu.xml', + 'crm_action_rule_view.xml', + 'crm_lead_view.xml', + 'crm_lead_menu.xml', + + 'crm_meeting_view.xml', + 'crm_meeting_menu.xml', + + 'crm_phonecall_view.xml', + 'crm_phonecall_menu.xml', + + 'crm_opportunity_view.xml', + 'crm_opportunity_menu.xml', + + 'security/crm_security.xml', + 'security/ir.model.access.csv', - 'crm_helpdesk_view.xml', - 'crm_helpdesk_menu.xml', + 'report/crm_lead_report_view.xml', + 'report/crm_phonecall_report_view.xml', - 'security/crm_security.xml', - 'security/ir.model.access.csv', - - 'report/crm_report_view.xml', - 'report/crm_claim_report_view.xml', - 'report/crm_lead_report_view.xml', - 'report/crm_fundraising_report_view.xml', - 'report/crm_opportunity_report_view.xml' , - 'report/crm_phonecall_report_view.xml', - - 'process/crm_configuration_process.xml', - ], + 'process/crm_configuration_process.xml', + 'crm_installer_view.xml' + ], 'demo_xml': [ - 'crm_demo.xml', - 'crm_claims_demo.xml', - 'crm_fund_demo.xml', - 'crm_helpdesk_demo.xml', - 'crm_lead_demo.xml', - 'crm_meeting_demo.xml', - 'crm_opportunity_demo.xml', + 'crm_demo.xml', + 'crm_lead_demo.xml', + 'crm_meeting_demo.xml', + 'crm_opportunity_demo.xml', 'crm_phonecall_demo.xml' - ], - 'installable': True, - 'active': False, - 'certificate': '0079056041421', + ], + 'test': [ + 'test/test_crm_lead.yml', + 'test/test_crm_meeting.yml', + 'test/test_crm_opportunity.yml', + 'test/test_crm_phonecall.yml', + ], + 'installable': True, + 'active': False, + 'certificate': '0079056041421', } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/crm.py b/addons/crm/crm.py index bca7ed1a9af..992fe2464c5 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -45,6 +45,273 @@ AVAILABLE_PRIORITIES = [ ('5', 'Lowest'), ] +class crm_case(object): + """A simple python class to be used for common functions """ + + def _get_default_partner_address(self, cr, uid, context): + """Gives id of default address for current user + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + if not context.get('portal', False): + return False + return self.pool.get('res.users').browse(cr, uid, uid, context).address_id.id + + def _get_default_partner(self, cr, uid, context): + """Gives id of partner for current user + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + if not context.get('portal', False): + return False + user = self.pool.get('res.users').browse(cr, uid, uid, context) + if not user.address_id: + return False + return user.address_id.partner_id.id + + def _get_default_email(self, cr, uid, context): + """Gives default email address for current user + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + if not context.get('portal', False): + return False + user = self.pool.get('res.users').browse(cr, uid, uid, context) + if not user.address_id: + return False + return user.address_id.email + + def _get_default_user(self, cr, uid, context): + """Gives current user id + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + if context.get('portal', False): + return False + return uid + + def _get_section(self, cr, uid, context): + """Gives section id for current User + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + return user.context_section_id.id or False + + def stage_next(self, cr, uid, ids, context=None): + """This function computes next stage for case from its current stage + using available stage for that case type + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case IDs + @param context: A standard dictionary for contextual values""" + if not context: + context = {} + + s = self.get_stage_dict(cr, uid, ids, context=context) + for case in self.browse(cr, uid, ids, context): + section = (case.section_id.id or False) + if section in s: + st = case.stage_id.id or False + if st in s[section]: + self.write(cr, uid, [case.id], {'stage_id': s[section][st]}) + return True + + def get_stage_dict(self, cr, uid, ids, context=None): + """This function gives dictionary for stage according to stage levels + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case IDs + @param context: A standard dictionary for contextual values""" + if not context: + context = {} + + sid = self.pool.get('crm.case.stage').search(cr, uid, \ + [('object_id.model', '=', self._name)], context=context) + s = {} + previous = {} + for stage in self.pool.get('crm.case.stage').browse(cr, uid, sid, context=context): + section = stage.section_id.id or False + s.setdefault(section, {}) + s[section][previous.get(section, False)] = stage.id + previous[section] = stage.id + return s + + def stage_previous(self, cr, uid, ids, context=None): + """This function computes previous stage for case from its current stage + using available stage for that case type + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case IDs + @param context: A standard dictionary for contextual values""" + if not context: + context = {} + + s = self.get_stage_dict(cr, uid, ids, context=context) + for case in self.browse(cr, uid, ids, context): + section = (case.section_id.id or False) + + if section in s: + st = case.stage_id.id or False + s[section] = dict([(v, k) for (k, v) in s[section].iteritems()]) + if st in s[section]: + self.write(cr, uid, [case.id], {'stage_id': s[section][st]}) + return True + + def onchange_partner_id(self, cr, uid, ids, part, email=False): + """This function returns value of partner address based on partner + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case IDs + @param part: Partner's id + @email: Partner's email ID + """ + if not part: + return {'value': {'partner_address_id': False, + 'email_from': False, + }} + addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact']) + data = {'partner_address_id': addr['contact']} + data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value']) + return {'value': data} + + def onchange_partner_address_id(self, cr, uid, ids, add, email=False): + """This function returns value of partner email based on Partner Address + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case IDs + @param add: Id of Partner's address + @email: Partner's email ID + """ + if not add: + return {'value': {'email_from': False}} + address = self.pool.get('res.partner.address').browse(cr, uid, add) + return {'value': {'email_from': address.email}} + + def case_open(self, cr, uid, ids, *args): + """Opens Case + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case Ids + @param *args: Tuple Value for additional Params + """ + cases = self.browse(cr, uid, ids) + self._history(cr, uid, cases, _('Open')) + for case in cases: + data = {'state': 'open', 'active': True} + if not case.user_id: + data['user_id'] = uid + self.write(cr, uid, case.id, data) + self._action(cr, uid, cases, 'open') + return True + + def case_close(self, cr, uid, ids, *args): + """Closes Case + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case Ids + @param *args: Tuple Value for additional Params + """ + cases = self.browse(cr, uid, ids) + cases[0].state # to fill the browse record cache + self._history(cr, uid, cases, _('Close')) + self.write(cr, uid, ids, {'state': 'done', + 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'), + }) + # + # We use the cache of cases to keep the old case state + # + self._action(cr, uid, cases, 'done') + return True + + def case_escalate(self, cr, uid, ids, *args): + """Escalates case to top level + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case Ids + @param *args: Tuple Value for additional Params + """ + cases = self.browse(cr, uid, ids) + for case in cases: + data = {'active': True, 'user_id': False} + + if case.section_id.parent_id: + data['section_id'] = case.section_id.parent_id.id + if case.section_id.parent_id.user_id: + data['user_id'] = case.section_id.parent_id.user_id.id + else: + raise osv.except_osv(_('Error !'), _('You can not escalate this case.\nYou are already at the top level.')) + self.write(cr, uid, [case.id], data) + cases = self.browse(cr, uid, ids) + self._history(cr, uid, cases, _('Escalate')) + self._action(cr, uid, cases, 'escalate') + return True + + def case_cancel(self, cr, uid, ids, *args): + """Cancels Case + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case Ids + @param *args: Tuple Value for additional Params + """ + cases = self.browse(cr, uid, ids) + cases[0].state # to fill the browse record cache + self._history(cr, uid, cases, _('Cancel')) + self.write(cr, uid, ids, {'state': 'cancel', + 'active': True}) + self._action(cr, uid, cases, 'cancel') + return True + + def case_pending(self, cr, uid, ids, *args): + """Marks case as pending + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case Ids + @param *args: Tuple Value for additional Params + """ + cases = self.browse(cr, uid, ids) + cases[0].state # to fill the browse record cache + self._history(cr, uid, cases, _('Pending')) + self.write(cr, uid, ids, {'state': 'pending', 'active': True}) + self._action(cr, uid, cases, 'pending') + return True + + def case_reset(self, cr, uid, ids, *args): + """Resets case as draft + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of case Ids + @param *args: Tuple Value for additional Params + """ + cases = self.browse(cr, uid, ids) + cases[0].state # to fill the browse record cache + self._history(cr, uid, cases, _('Draft')) + self.write(cr, uid, ids, {'state': 'draft', 'active': True}) + self._action(cr, uid, cases, 'draft') + return True + + class crm_case_section(osv.osv): """Sales Team""" @@ -59,6 +326,7 @@ class crm_case_section(osv.osv): true, it will allow you to hide the sales team without removing it."), 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"), 'user_id': fields.many2one('res.users', 'Responsible User'), + 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'), 'reply_to': fields.char('Reply-To', size=64, help="The email address put \ in the 'Reply-To' of all emails sent by Open ERP about cases in this sales team"), 'parent_id': fields.many2one('crm.case.section', 'Parent Team'), @@ -78,15 +346,16 @@ class crm_case_section(osv.osv): ] def _check_recursion(self, cr, uid, ids): + """ - Checks for recursion level for sections + Checks for recursion level for sales team @param self: The object pointer @param cr: the current row, from the database cursor, @param uid: the current user’s ID for security checks, - @param ids: List of section ids + @param ids: List of Sales team ids """ level = 100 - + while len(ids): cr.execute('select distinct parent_id from crm_case_section where id =ANY(%s)', (ids,)) ids = filter(None, map(lambda x: x[0], cr.fetchall())) @@ -97,7 +366,7 @@ class crm_case_section(osv.osv): return True _constraints = [ - (_check_recursion, 'Error ! You cannot create recursive sections.', ['parent_id']) + (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id']) ] def name_get(self, cr, uid, ids, context=None): @@ -105,7 +374,7 @@ class crm_case_section(osv.osv): @param self: The object pointer @param cr: the current row, from the database cursor, @param uid: the current user’s ID for security checks, - @param ids: List of section ids + @param ids: List of sales team ids """ if not context: context = {} @@ -136,8 +405,8 @@ class crm_case_categ(osv.osv): 'section_id': fields.many2one('crm.case.section', 'Sales Team'), 'object_id': fields.many2one('ir.model', 'Object Name'), } - def _find_object_id(self, cr, uid, context=None): + def _find_object_id(self, cr, uid, context=None): """Finds id for case object @param self: The object pointer @param cr: the current row, from the database cursor, @@ -151,8 +420,8 @@ class crm_case_categ(osv.osv): _defaults = { 'object_id' : _find_object_id - } + } crm_case_categ() @@ -238,501 +507,6 @@ def _links_get(self, cr, uid, context=None): res = obj.read(cr, uid, ids, ['object', 'name'], context) return [(r['object'], r['name']) for r in res] -class crm_case(osv.osv): - """ CRM Case """ - - _name = "crm.case" - _description = "Case" - - def _email_last(self, cursor, user, ids, name, arg, context=None): - """Return last email from History - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case’s IDs - @param context: A standard dictionary for contextual values - """ - res = {} - for case in self.browse(cursor, user, ids): - if case.history_line: - res[case.id] = case.history_line[0].description - else: - res[case.id] = False - return res - - def copy(self, cr, uid, id, default=None, context=None): - """Overrides orm copy method - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case’s IDs - @param context: A standard dictionary for contextual values - """ - if not context: - context = {} - if not default: - default = {} - default.update({'state': 'draft', 'id': False}) - return super(crm_case, self).copy(cr, uid, id, default, context) - - def _get_log_ids(self, cr, uid, ids, field_names, arg, context=None): - """Gets id for case log from history of particular case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of Case IDs - @param context: A standard dictionary for contextual values - """ - if not context: - context = {} - - result = {} - history_obj = False - model_obj = self.pool.get('ir.model') - - if 'history_line' in field_names: - history_obj = self.pool.get('crm.case.history') - name = 'history_line' - - if 'log_ids' in field_names: - history_obj = self.pool.get('crm.case.log') - name = 'log_ids' - - if not history_obj: - return result - - for case in self.browse(cr, uid, ids, context): - model_ids = model_obj.search(cr, uid, [('model', '=', case._name)]) - history_ids = history_obj.search(cr, uid, [('model_id', '=', model_ids[0]), \ - ('res_id', '=', case.id)]) - if history_ids: - result[case.id] = {name: history_ids} - else: - result[case.id] = {name: []} - - return result - - _columns = { - 'id': fields.integer('ID', readonly=True), - 'name': fields.char('Description', size=1024, required=True), - 'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."), - 'description': fields.text('Description'), - 'section_id': fields.many2one('crm.case.section', 'Sales Team', \ - select=True, help='Sales team to which Case belongs to.\ - Define Responsible user and Email account for mail gateway.'), - 'email_from': fields.char('Email', size=128, help="These people will receive email."), - 'email_cc': fields.text('Watchers Emails', size=252 , help="These people\ - will receive a copy of the future" \ - " communication between partner and users by email"), - 'probability': fields.float('Probability'), - 'email_last': fields.function(_email_last, method=True, - string='Latest E-Mail', type='text'), - 'partner_id': fields.many2one('res.partner', 'Partner'), - 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \ - domain="[('partner_id','=',partner_id)]"), - 'create_date': fields.datetime('Creation Date' , readonly=True), - 'write_date': fields.datetime('Update Date' , readonly=True), - 'date_deadline': fields.date('Deadline'), - 'user_id': fields.many2one('res.users', 'Responsible'), - 'history_line': fields.function(_get_log_ids, method=True, type='one2many', \ - multi="history_line", relation="crm.case.history", string="Communication"), - 'log_ids': fields.function(_get_log_ids, method=True, type='one2many', \ - multi="log_ids", relation="crm.case.log", string="Logs History"), - 'stage_id': fields.many2one ('crm.case.stage', 'Stage', \ - domain="[('section_id','=',section_id),\ - ('object_id.model', '=', 'crm.opportunity')]"), - 'state': fields.selection(AVAILABLE_STATES, 'State', size=16, readonly=True, - help='The state is set to \'Draft\', when a case is created.\ - \nIf the case is in progress the state is set to \'Open\'.\ - \nWhen the case is over, the state is set to \'Done\'.\ - \nIf the case needs to be reviewed then the state is set to \'Pending\'.'), - 'company_id': fields.many2one('res.company', 'Company'), - } - def _get_default_partner_address(self, cr, uid, context): - """Gives id of default address for current user - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - if not context.get('portal', False): - return False - return self.pool.get('res.users').browse(cr, uid, uid, context).address_id.id - - def _get_default_partner(self, cr, uid, context): - """Gives id of partner for current user - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - if not context.get('portal', False): - return False - user = self.pool.get('res.users').browse(cr, uid, uid, context) - if not user.address_id: - return False - return user.address_id.partner_id.id - - def _get_default_email(self, cr, uid, context): - """Gives default email address for current user - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - if not context.get('portal', False): - return False - user = self.pool.get('res.users').browse(cr, uid, uid, context) - if not user.address_id: - return False - return user.address_id.email - - def _get_default_user(self, cr, uid, context): - """Gives current user id - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - if context.get('portal', False): - return False - return uid - - def _get_section(self, cr, uid, context): - """Gives section id for current User - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - return user.context_section_id.id or False - - _defaults = { - 'active': lambda *a: 1, - 'user_id': _get_default_user, - 'partner_id': _get_default_partner, - 'partner_address_id': _get_default_partner_address, - 'email_from': _get_default_email, - 'state': lambda *a: 'draft', - 'section_id': _get_section, - 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.case', context=c), - } - - _order = 'date_deadline desc, create_date desc,id desc' - - def unlink(self, cr, uid, ids, context=None): - """Overrides orm unlink method - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param context: A standard dictionary for contextual values - """ - if not context: - context = {} - - hist_obj = self.pool.get('crm.case.history') - log_obj = self.pool.get('crm.case.log') - - for case in self.browse(cr, uid, ids, context): - if (not case.section_id.allow_unlink) and (case.state <> 'draft'): - raise osv.except_osv(_('Warning !'), - _('You can not delete this case. You should better cancel it.')) - - # Also removing history and logs - history_ids = map(lambda x: x.id, case.history_line) - log_ids = map(lambda x: x.id, case.log_ids) - hist_obj.unlink(cr, uid, history_ids, context=context) - log_obj.unlink(cr, uid, log_ids, context=context) - return super(crm_case, self).unlink(cr, uid, ids, context) - - def stage_next(self, cr, uid, ids, context=None): - """This function computes next stage for case from its current stage - using available stage for that case type - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param context: A standard dictionary for contextual values""" - if not context: - context = {} - - s = self.get_stage_dict(cr, uid, ids, context=context) - for case in self.browse(cr, uid, ids, context): - section = (case.section_id.id or False) - if section in s: - st = case.stage_id.id or False - if st in s[section]: - self.write(cr, uid, [case.id], {'stage_id': s[section][st]}) - return True - - def get_stage_dict(self, cr, uid, ids, context=None): - """This function gives dictionary for stage according to stage levels - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param context: A standard dictionary for contextual values""" - if not context: - context = {} - - sid = self.pool.get('crm.case.stage').search(cr, uid, \ - [('object_id.model', '=', self._name)], context=context) - s = {} - previous = {} - for stage in self.pool.get('crm.case.stage').browse(cr, uid, sid, context=context): - section = stage.section_id.id or False - s.setdefault(section, {}) - s[section][previous.get(section, False)] = stage.id - previous[section] = stage.id - return s - - def stage_previous(self, cr, uid, ids, context=None): - """This function computes previous stage for case from its current stage - using available stage for that case type - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param context: A standard dictionary for contextual values""" - if not context: - context = {} - - s = self.get_stage_dict(cr, uid, ids, context=context) - for case in self.browse(cr, uid, ids, context): - section = (case.section_id.id or False) - - if section in s: - st = case.stage_id.id or False - s[section] = dict([(v, k) for (k, v) in s[section].iteritems()]) - if st in s[section]: - self.write(cr, uid, [case.id], {'stage_id': s[section][st]}) - return True - - def __history(self, cr, uid, cases, keyword, history=False, email=False, details=None, email_from=False, message_id=False, context={}): - """ - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param cases: a browse record list - @param keyword: Case action keyword e.g.: If case is closed "Close" keyword is used - @param history: Value True/False, If True it makes entry in case History otherwise in Case Log - @param email: Email address if any - @param details: Details of case history if any - @param context: A standard dictionary for contextual values""" - if not context: - context = {} - - # The mailgate sends the ids of the cases and not the object list - if all(isinstance(case_id, (int, long)) for case_id in cases) and context.get('model'): - cases = self.pool.get(context['model']).browse(cr, uid, cases, context=context) - - model_obj = self.pool.get('ir.model') - obj = self.pool.get('crm.case.log') - for case in cases: - model_ids = model_obj.search(cr, uid, [('model', '=', case._name)]) - data = { - 'name': keyword, - 'user_id': uid, - 'date': time.strftime('%Y-%m-%d %H:%M:%S'), - 'model_id' : model_ids and model_ids[0] or False, - 'res_id': case.id, - 'section_id': case.section_id.id, - 'message_id':message_id - } - - if history: - obj = self.pool.get('crm.case.history') - data['description'] = details or case.description - data['email_to'] = email or \ - (case.section_id and case.section_id.reply_to) or \ - (case.user_id and case.user_id.address_id and \ - case.user_id.address_id.email) or tools.config.get('email_from', False) - data['email_from'] = email_from or \ - (case.section_id and case.section_id.reply_to) or \ - (case.user_id and case.user_id.address_id and \ - case.user_id.address_id.email) or tools.config.get('email_from', False) - res = obj.create(cr, uid, data, context) - return True - - _history = __history - history = __history - - def create(self, cr, uid, *args, **argv): - """Overrides orm create method - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param *args: Tuple Value for additional Params - @param **argv: Dictionay of Keyword Params - """ - res = super(crm_case, self).create(cr, uid, *args, **argv) - cases = self.browse(cr, uid, [res]) - cases[0].state # to fill the browse record cache - self._action(cr, uid, cases, 'draft') - return res - - def add_reply(self, cursor, user, ids, context=None): - """This function finds last email and gives its description value for reply mail - @param self: The object pointer - @param cursor: the current row, from the database cursor, - @param user: the current user’s ID for security checks - @param ids: List of case IDs - @param context: A standard dictionary for contextual values - """ - for case in self.browse(cursor, user, ids, context=context): - if case.email_last: - description = case.email_last - self.write(cursor, user, case.id, { - 'description': '> ' + description.replace('\n', '\n> '), - }, context=context) - return True - - def onchange_partner_id(self, cr, uid, ids, part, email=False): - """This function returns value of partner address based on partner - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param part: Partner's id - @email: Partner's email ID - """ - if not part: - return {'value': {'partner_address_id': False, - 'email_from': False, - }} - addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact']) - data = {'partner_address_id': addr['contact']} - data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value']) - return {'value': data} - - def onchange_partner_address_id(self, cr, uid, ids, add, email=False): - """This function returns value of partner email based on Partner Address - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param add: Id of Partner's address - @email: Partner's email ID - """ - if not add: - return {'value': {'email_from': False}} - address = self.pool.get('res.partner.address').browse(cr, uid, add) - return {'value': {'email_from': address.email}} - - def case_close(self, cr, uid, ids, *args): - """Closes Case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ - cases = self.browse(cr, uid, ids) - cases[0].state # to fill the browse record cache - self.__history(cr, uid, cases, _('Close')) - self.write(cr, uid, ids, {'state': 'done', - 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'), - 'probability' : 100.0}) - # - # We use the cache of cases to keep the old case state - # - self._action(cr, uid, cases, 'done') - return True - - def case_escalate(self, cr, uid, ids, *args): - """Escalates case to top level - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ - cases = self.browse(cr, uid, ids) - for case in cases: - data = {'active': True, 'user_id': False} - - if case.section_id.parent_id: - data['section_id'] = case.section_id.parent_id.id - if case.section_id.parent_id.user_id: - data['user_id'] = case.section_id.parent_id.user_id.id - else: - raise osv.except_osv(_('Error !'), _('You can not escalate this case.\nYou are already at the top level.')) - self.write(cr, uid, [case.id], data) - cases = self.browse(cr, uid, ids) - self.__history(cr, uid, cases, _('Escalate')) - self._action(cr, uid, cases, 'escalate') - return True - - def case_open(self, cr, uid, ids, *args): - """Opens Case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ - cases = self.browse(cr, uid, ids) - self.__history(cr, uid, cases, _('Open')) - - for case in cases: - data = {'state': 'open', 'active': True} - if not case.user_id: - data['user_id'] = uid - self.write(cr, uid, case.id, data) - self._action(cr, uid, cases, 'open') - return True - - def case_cancel(self, cr, uid, ids, *args): - """Cancels Case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ - cases = self.browse(cr, uid, ids) - cases[0].state # to fill the browse record cache - self.__history(cr, uid, cases, _('Cancel')) - self.write(cr, uid, ids, {'state': 'cancel', - 'active': True, - 'probability' : 0.0}) - self._action(cr, uid, cases, 'cancel') - return True - - def case_pending(self, cr, uid, ids, *args): - """Marks case as pending - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ - cases = self.browse(cr, uid, ids) - cases[0].state # to fill the browse record cache - self.__history(cr, uid, cases, _('Pending')) - self.write(cr, uid, ids, {'state': 'pending', 'active': True}) - self._action(cr, uid, cases, 'pending') - return True - - def case_reset(self, cr, uid, ids, *args): - """Resets case as draft - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ - cases = self.browse(cr, uid, ids) - cases[0].state # to fill the browse record cache - self.__history(cr, uid, cases, _('Draft')) - self.write(cr, uid, ids, {'state': 'draft', 'active': True}) - self._action(cr, uid, cases, 'draft') - return True - -crm_case() - class crm_case_log(osv.osv): """ Case Communication History """ diff --git a/addons/crm/crm_action_rule.py b/addons/crm/crm_action_rule.py index 718b12bb9c3..82c60e64f23 100644 --- a/addons/crm/crm_action_rule.py +++ b/addons/crm/crm_action_rule.py @@ -37,7 +37,7 @@ import crm class case(osv.osv): """ Case """ - _inherit = 'crm.case' + _inherit = 'mailgate.thread' _description = 'case' _columns = { @@ -89,7 +89,7 @@ class case(osv.osv): attach_to_send = None if attach: - attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', 'crm.case'), ('res_id', '=', case.id)]) + attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', 'mailgate.thread'), ('res_id', '=', case.id)]) attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname','datas']) attach_to_send = map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send) @@ -152,6 +152,18 @@ class base_action_rule(osv.osv): """ Base Action Rule """ _inherit = 'base.action.rule' _description = 'Action Rules' + + _columns = { + 'trg_section_id': fields.many2one('crm.case.section', 'Sales Team'), + 'trg_max_history': fields.integer('Maximum Communication History'), + 'trg_categ_id': fields.many2one('crm.case.categ', 'Category'), + 'regex_history' : fields.char('Regular Expression on Case History', size=128), + 'act_section_id': fields.many2one('crm.case.section', 'Set Team to'), + 'act_categ_id': fields.many2one('crm.case.categ', 'Set Category to'), + 'act_mail_to_partner': fields.boolean('Mail to partner',help="Check \ +this if you want the rule to send an email to the partner."), + } + def email_send(self, cr, uid, obj, emails, body, emailfrom=tools.config.get('email_from',False), context={}): body = self.format_mail(obj, body) @@ -183,16 +195,18 @@ class base_action_rule(osv.osv): ok = ok and (not action.trg_section_id or action.trg_section_id.id==obj.section_id.id) if hasattr(obj, 'categ_id'): ok = ok and (not action.trg_categ_id or action.trg_categ_id.id==obj.categ_id.id) - if hasattr(obj, 'history_line'): - ok = ok and (not action.trg_max_history or action.trg_max_history<=(len(obj.history_line)+1)) - reg_history = action.regex_history - result_history = True - if reg_history: - ptrn = re.compile(str(reg_history)) - if obj.history_line: - _result = ptrn.search(str(obj.history_line[0].description)) - if not _result: - result_history = False + +# TODO: history_line is removed +# if hasattr(obj, 'history_line'): +# ok = ok and (not action.trg_max_history or action.trg_max_history<=(len(obj.history_line)+1)) +# reg_history = action.regex_history +# result_history = True +# if reg_history: +# ptrn = re.compile(str(reg_history)) +# if obj.history_line: +# _result = ptrn.search(str(obj.history_line[0].description)) +# if not _result: +# result_history = False regex_h = not reg_history or result_history ok = ok and regex_h return ok @@ -207,7 +221,7 @@ class base_action_rule(osv.osv): res = super(base_action_rule, self).do_action(cr, uid, action, model_obj, obj, context=context) write = {} - if hasattr(action, act_section_id) and action.act_section_id: + if hasattr(action, 'act_section_id') and action.act_section_id: obj.section_id = action.act_section_id write['section_id'] = action.act_section_id.id @@ -231,13 +245,6 @@ class base_action_rule(osv.osv): return True -base_action_rule() - -class base_action_rule_line(osv.osv): - """ Base Action Rule Line """ - _inherit = 'base.action.rule.line' - _description = 'Base Action Rule Line' - def state_get(self, cr, uid, context={}): """@param self: The object pointer @@ -245,7 +252,7 @@ class base_action_rule_line(osv.osv): @param uid: the current user’s ID for security checks, @param context: A standard dictionary for contextual values """ - res = super(base_action_rule_line, self).state_get(cr, uid, context=context) + res = super(base_action_rule, self).state_get(cr, uid, context=context) return res + [('escalate','Escalate')] + crm.AVAILABLE_STATES def priority_get(self, cr, uid, context={}): @@ -255,18 +262,10 @@ class base_action_rule_line(osv.osv): @param uid: the current user’s ID for security checks, @param context: A standard dictionary for contextual values """ - res = super(base_action_rule_line, self).priority_get(cr, uid, context=context) + res = super(base_action_rule, self).priority_get(cr, uid, context=context) return res + crm.AVAILABLE_PRIORITIES - _columns = { - 'trg_section_id': fields.many2one('crm.case.section', 'Sales Team'), - 'trg_max_history': fields.integer('Maximum Communication History'), - 'trg_categ_id': fields.many2one('crm.case.categ', 'Category'), - 'regex_history' : fields.char('Regular Expression on Case History', size=128), - 'act_section_id': fields.many2one('crm.case.section', 'Set Team to'), - 'act_categ_id': fields.many2one('crm.case.categ', 'Set Category to'), - 'act_mail_to_partner': fields.boolean('Mail to partner',help="Check this \ - if you want the rule to send an email to the partner."), - } -base_action_rule_line() +base_action_rule() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/crm_action_rule_view.xml b/addons/crm/crm_action_rule_view.xml index 78767a2f795..96f8d2f04bf 100644 --- a/addons/crm/crm_action_rule_view.xml +++ b/addons/crm/crm_action_rule_view.xml @@ -3,9 +3,9 @@ - base.action.rule.line.form.inherit - base.action.rule.line - + base.action.rule.form.inherit + base.action.rule + form @@ -19,9 +19,9 @@ - base.action.rule.line.form2.inherit - base.action.rule.line - + base.action.rule.form2.inherit + base.action.rule + form @@ -35,17 +35,29 @@ - base.action.rule.line.form3.inherit - base.action.rule.line - + base.action.rule.form3.inherit + base.action.rule + form + + + + + + base.action.rule.form4.inherit + base.action.rule + + form + + + diff --git a/addons/crm/crm_claim.py b/addons/crm/crm_claim.py deleted file mode 100644 index 43ec4fe9895..00000000000 --- a/addons/crm/crm_claim.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-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 fields, osv -import crm - -class crm_claim(osv.osv): - """ - Crm claim - """ - _name = "crm.claim" - _description = "Claim Cases" - _order = "id desc" - _inherit = 'crm.case' - _columns = { - 'date_closed': fields.datetime('Closed', readonly=True), - 'date': fields.datetime('Date'), - 'ref' : fields.reference('Reference', selection=crm._links_get, size=128), - 'ref2' : fields.reference('Reference 2', selection=crm._links_get, size=128), - 'canal_id': fields.many2one('res.partner.canal', 'Channel',\ - help="The channels represent the different communication\ - modes available with the customer." \ - " With each commercial opportunity, you can indicate the\ - canall which is this opportunity source."), - 'planned_revenue': fields.float('Planned Revenue'), - 'planned_cost': fields.float('Planned Costs'), - 'som': fields.many2one('res.partner.som', 'State of Mind', \ - help="The minds states allow to define a value scale which represents" \ - "the partner mentality in relation to our services.The scale has" \ - "to be created with a factor for each level from 0 (Very dissatisfied) \ - to 10 (Extremely satisfied)."), - 'categ_id': fields.many2one('crm.case.categ', 'Category',\ - domain="[('section_id','=',section_id),\ - ('object_id.model', '=', 'crm.claim')]"), - 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'), - 'type_id': fields.many2one('crm.case.resource.type', 'Claim Type',\ - domain="[('section_id','=',section_id),\ - ('object_id.model', '=', 'crm.claim')]"), - - 'partner_name': fields.char("Employee's Name", size=64), - 'partner_mobile': fields.char('Mobile', size=32), - 'partner_phone': fields.char('Phone', size=32), - 'stage_id': fields.many2one ('crm.case.stage', 'Stage',\ - domain="[('section_id','=',section_id),\ - ('object_id.model', '=', 'crm.claim')]"), - 'probability': fields.float('Probability (%)'), - } - - _defaults = { - 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0], - } - -crm_claim() - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/crm/crm_data.xml b/addons/crm/crm_data.xml index 6dd53747c54..cfff95888cc 100644 --- a/addons/crm/crm_data.xml +++ b/addons/crm/crm_data.xml @@ -2,11 +2,6 @@ - - Case - crm.case - - CRM: Create Case crm_case_draft diff --git a/addons/crm/crm_fund_view.xml b/addons/crm/crm_fund_view.xml deleted file mode 100644 index 20d3f5a286a..00000000000 --- a/addons/crm/crm_fund_view.xml +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - - - Fundraising Categories - crm.case.categ - form - - [('object_id.model', '=', 'crm.fundraising')] - {'object_id':'crm.fundraising'} - - - - - - - - Fundraising Stages - crm.case.stage - form - - [('object_id.model', '=', 'crm.fundraising')] - {'object_id':'crm.fundraising'} - - - - - - - - Fundraising Resource Type - crm.case.resource.type - form - - [('object_id.model', '=', 'crm.fundraising')] - {'object_id':'crm.fundraising'} - - - - - - - - - CRM - Funds Tree - crm.fundraising - tree - - - - - - - - - - -
Note: If you are interested please reply this - mail and keep only your response from options YES, NO - and MAYBE.
From:
+ + + + + '; + + foreach ($post as $key => $value) + { + $body .= " + + + + "; + } + + $body .= '
$key:$value
'; + + $header = 'MEME-version : 1.0'."\r\n"; + $header .= 'Content-type: text/html; charset=iso-8859-1'."\r\n"; + $header .= 'From: '.$post['name'].' <'.$post['email'].'>'."\r\n"; + $header .= 'Reply-To: '.$post['name'].' <'.$post['email'].'>'."\r\n"; + $header .= 'Return-Path: '.$post['name'].' <'.$post['email'].'>'."\r\n"; + + if (mail($this->to, $this->subject, $body, $header)) + { + echo "

Thank You, Your information has been successfully submitted.


We will be in touch very soon.

"; + + } + else + { + echo "

Sorry, Due to some problem Your information has not been submitted.

"; + } + + } + } + + function xmlCallTo($usr, $password, $database, $server, $post) + { + $user = $usr; + $pass = $password; + $db = $database; + $server_url = $server; //'http://localhost:8069/xmlrpc/' + + $client = new xmlrpc_client($server_url.'common'); + + $msg = new xmlrpcmsg('login'); + $msg->addParam(new xmlrpcval($db, "string")); + $msg->addParam(new xmlrpcval($user, "string")); + $msg->addParam(new xmlrpcval($pass, "string")); + + + $res = &$client->send($msg); + + if(!$res->faultCode()){ + + $val = $res->value(); + $id = $val->scalarval(); + + if (empty($id)){ + + echo "Connection error = "; + exit; + } + else + { + $client2 = new xmlrpc_client($server_url.'object'); + + $key = array(new xmlrpcval(array(new xmlrpcval("code", "string"), new xmlrpcval("=", "string"), new xmlrpcval($post['country'], "string")),"array"),); + $msg = new xmlrpcmsg('execute'); + $msg->addParam(new xmlrpcval($db, "string")); + $msg->addParam(new xmlrpcval($id, "int")); + $msg->addParam(new xmlrpcval($pass, "string")); + $msg->addParam(new xmlrpcval("res.country","string")); + $msg->addParam(new xmlrpcval("search", "string")); + $msg->addParam(new xmlrpcval($key, "array")); + $msg->addParam(new xmlrpcval(0, "int")); + $msg->addParam(new xmlrpcval(1, "int")); + + $res = &$client2->send($msg); + + $val = $res->value()->scalarval(); + + $countryId = $val[0]->scalarval(); + + $val = array ("name" => new xmlrpcval($post['company'],"string"), + "title" => new xmlrpcval($post['salutation'], "string"), + "email_from" => new xmlrpcval($post['email'], "string"), + "phone" => new xmlrpcval($post['phone'], "string"), + "partner_name" => new xmlrpcval($post['name'], "string"), + "function_name" => new xmlrpcval($post["jobtitle"], "string"), + "zip" => new xmlrpcval($post['zip'], "string"), + "stage_id" => new xmlrpcval(6, "int"), + "city" => new xmlrpcval($post['city'], "string"), + "country_id" => new xmlrpcval($countryId, "int"), + "state" => new xmlrpcval("draft", "string"), + "user_id" => new xmlrpcval(false, "boolean"), + "description" => new xmlrpcval("No.of Employees: ".$post['employees']."\nState: ".$post['state']."\nIndustry: ".$post['industry']."\nReasons: ".$post['reasons']."\nAbout: ".$post['about']."\nQuestions: ".stripslashes($post['questions'])."\nUpdate: ".$post['update'], "string") + ); + + $msg = new xmlrpcmsg('execute'); + $msg->addParam(new xmlrpcval($db, "string")); + $msg->addParam(new xmlrpcval($id, "int")); + $msg->addParam(new xmlrpcval($pass, "string")); + $msg->addParam(new xmlrpcval("crm.lead", "string")); + $msg->addParam(new xmlrpcval("create", "string")); + $msg->addParam(new xmlrpcval($val, "struct")); + + + $res2 = &$client2->send($msg); + + if(!$res2->faultCode()) + { + $readVal = $res2->value()->scalarval(); + + if (!empty($readVal)) + { + $val = array ( "description" => new xmlrpcval("About: ".$post['about']."\nQuestions: ".$post['questions']), + "model_id" => new xmlrpcval(276, "int"), + "res_id" => new xmlrpcval($readVal,"int"), + "email_from" => new xmlrpcval($post['email'], "string"), + "email_to" => new xmlrpcval("sales@openerp.com", "string") + ); + + + $msg = new xmlrpcmsg('execute'); + $msg->addParam(new xmlrpcval($db, "string")); + $msg->addParam(new xmlrpcval($id, "int")); + $msg->addParam(new xmlrpcval($pass, "string")); + $msg->addParam(new xmlrpcval("crm.case.history", "string")); + $msg->addParam(new xmlrpcval("create", "string")); + $msg->addParam(new xmlrpcval($val, "struct")); + + $res2 = &$client2->send($msg); + + //echo "
Successfully created lead"; + echo "

Thank You for your interest in openerp, we'll respond to your request shortly.


"; + if(strstr($post["about"],"Book")) { + echo ''; + } + } + else + { + echo "
Lead is not created"; + } + } + else + { + echo "
Problem in message sending for create lead"; + } + } + } + else + { + echo "
Connection not established"; + } + } +} + + $arrData = array(); + $arrData = array_merge($arrData, (array)$_POST); + + $cnt = new Contact('sales5@openerp.com', 'Country: '.$arrData['country']. ' About: ' .$arrData['about']); + + /* This function use for sending mail on perticular mail account */ + /*$cnt->mailTo($arrData); */ + + /* This function use ceating lead in crm of opener erp database */ + $cnt->xmlCallTo('', '', '', '', $arrData); + + exit; +?> diff --git a/addons/crm/scripts/php/xmlrpc.inc b/addons/crm/scripts/php/xmlrpc.inc new file mode 100755 index 00000000000..78ac897e2ce --- /dev/null +++ b/addons/crm/scripts/php/xmlrpc.inc @@ -0,0 +1,3600 @@ + +// $Id: xmlrpc.inc,v 1.113 2006/01/22 23:55:57 ggiunta Exp $ + + +// Copyright (c) 1999,2000,2002 Edd Dumbill. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// * Neither the name of the "XML-RPC for PHP" nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +// OF THE POSSIBILITY OF SUCH DAMAGE. + + if(!function_exists('xml_parser_create')) + { + // For PHP 4 onward, XML functionality is always compiled-in on windows: + // no more need to dl-open it. It might have been compiled out on *nix... + if(strtoupper(substr(PHP_OS, 0, 3) != 'WIN')) + { + dl('xml.so'); + } + } + + // Try to be backward compat with php < 4.2 (are we not being nice ?) + if(substr(phpversion(), 0, 3) == '4.0' || @version_compare(substr(phpversion(), 0, 3) == '4.1')) + { + // give an opportunity to user to specify where to include other files from + if(!defined('PHP_XMLRPC_COMPAT_DIR')) + { + define('PHP_XMLRPC_COMPAT_DIR',dirname(__FILE__).'/compat/'); + } + if(substr(phpversion(), 0, 3) == '4.0') + { + include(PHP_XMLRPC_COMPAT_DIR."is_scalar.php"); + include(PHP_XMLRPC_COMPAT_DIR."array_key_exists.php"); + include(PHP_XMLRPC_COMPAT_DIR."version_compare.php"); + } + include(PHP_XMLRPC_COMPAT_DIR."var_export.php"); + include(PHP_XMLRPC_COMPAT_DIR."is_a.php"); + } + + // G. Giunta 2005/01/29: declare global these variables, + // so that xmlrpc.inc will work even if included from within a function + // Milosch: 2005/08/07 - explicitly request these via $GLOBALS where used. + $GLOBALS['xmlrpcI4']='i4'; + $GLOBALS['xmlrpcInt']='int'; + $GLOBALS['xmlrpcBoolean']='boolean'; + $GLOBALS['xmlrpcDouble']='double'; + $GLOBALS['xmlrpcString']='string'; + $GLOBALS['xmlrpcDateTime']='dateTime.iso8601'; + $GLOBALS['xmlrpcBase64']='base64'; + $GLOBALS['xmlrpcArray']='array'; + $GLOBALS['xmlrpcStruct']='struct'; + $GLOBALS['xmlrpcValue']='undefined'; + + $GLOBALS['xmlrpcTypes']=array( + $GLOBALS['xmlrpcI4'] => 1, + $GLOBALS['xmlrpcInt'] => 1, + $GLOBALS['xmlrpcBoolean'] => 1, + $GLOBALS['xmlrpcString'] => 1, + $GLOBALS['xmlrpcDouble'] => 1, + $GLOBALS['xmlrpcDateTime'] => 1, + $GLOBALS['xmlrpcBase64'] => 1, + $GLOBALS['xmlrpcArray'] => 2, + $GLOBALS['xmlrpcStruct'] => 3 + ); + + $GLOBALS['xmlrpc_valid_parents'] = array( + 'BOOLEAN' => array('VALUE'), + 'I4' => array('VALUE'), + 'INT' => array('VALUE'), + 'STRING' => array('VALUE'), + 'DOUBLE' => array('VALUE'), + 'DATETIME.ISO8601' => array('VALUE'), + 'BASE64' => array('VALUE'), + 'ARRAY' => array('VALUE'), + 'STRUCT' => array('VALUE'), + 'PARAM' => array('PARAMS'), + 'METHODNAME' => array('METHODCALL'), + 'PARAMS' => array('METHODCALL', 'METHODRESPONSE'), + 'MEMBER' => array('STRUCT'), + 'NAME' => array('MEMBER'), + 'DATA' => array('ARRAY'), + 'FAULT' => array('METHODRESPONSE'), + 'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'), + ); + + $GLOBALS['xmlEntities']=array( + 'amp' => '&', + 'quot' => '"', + 'lt' => '<', + 'gt' => '>', + 'apos' => "'" + ); + + // tables used for transcoding different charsets into us-ascii xml + + $GLOBALS['xml_iso88591_Entities']=array(); + $GLOBALS['xml_iso88591_Entities']['in'] = array(); + $GLOBALS['xml_iso88591_Entities']['out'] = array(); + for ($i = 0; $i < 32; $i++) + { + + $GLOBALS['xml_iso88591_Entities']['in'][] = chr($i); + $GLOBALS['xml_iso88591_Entities']['out'][] = "&#$i;"; + } + for ($i = 160; $i < 256; $i++) + { + $GLOBALS['xml_iso88591_Entities']['in'][] = chr($i); + $GLOBALS['xml_iso88591_Entities']['out'][] = "&#$i;"; + } + + /// @todo add to iso table the characters from cp_1252 range, i.e. 128 to 159. + /// These will NOT be present in true ISO-8859-1, but will save the unwary + /// windows user from sending junk. +/* +$cp1252_to_htmlent = + array( + '\x80'=>'€', '\x81'=>'?', '\x82'=>'‚', '\x83'=>'ƒ', + '\x84'=>'„', '\x85'=>'…', '\x86'=>'†', \x87'=>'‡', + '\x88'=>'ˆ', '\x89'=>'‰', '\x8A'=>'Š', '\x8B'=>'‹', + '\x8C'=>'Œ', '\x8D'=>'?', '\x8E'=>'Ž', '\x8F'=>'?', + '\x90'=>'?', '\x91'=>'‘', '\x92'=>'’', '\x93'=>'“', + '\x94'=>'”', '\x95'=>'•', '\x96'=>'–', '\x97'=>'—', + '\x98'=>'˜', '\x99'=>'™', '\x9A'=>'š', '\x9B'=>'›', + '\x9C'=>'œ', '\x9D'=>'?', '\x9E'=>'ž', '\x9F'=>'Ÿ' + ); +*/ + + $GLOBALS['xmlrpcerr']['unknown_method']=1; + $GLOBALS['xmlrpcstr']['unknown_method']='Unknown method'; + $GLOBALS['xmlrpcerr']['invalid_return']=2; + $GLOBALS['xmlrpcstr']['invalid_return']='Invalid return payload: enable debugging to examine incoming payload'; + $GLOBALS['xmlrpcerr']['incorrect_params']=3; + $GLOBALS['xmlrpcstr']['incorrect_params']='Incorrect parameters passed to method'; + $GLOBALS['xmlrpcerr']['introspect_unknown']=4; + $GLOBALS['xmlrpcstr']['introspect_unknown']="Can't introspect: method unknown"; + $GLOBALS['xmlrpcerr']['http_error']=5; + $GLOBALS['xmlrpcstr']['http_error']="Didn't receive 200 OK from remote server."; + $GLOBALS['xmlrpcerr']['no_data']=6; + $GLOBALS['xmlrpcstr']['no_data']='No data received from server.'; + $GLOBALS['xmlrpcerr']['no_ssl']=7; + $GLOBALS['xmlrpcstr']['no_ssl']='No SSL support compiled in.'; + $GLOBALS['xmlrpcerr']['curl_fail']=8; + $GLOBALS['xmlrpcstr']['curl_fail']='CURL error'; + $GLOBALS['xmlrpcerr']['invalid_request']=15; + $GLOBALS['xmlrpcstr']['invalid_request']='Invalid request payload'; + $GLOBALS['xmlrpcerr']['no_curl']=16; + $GLOBALS['xmlrpcstr']['no_curl']='No CURL support compiled in.'; + $GLOBALS['xmlrpcerr']['server_error']=17; + $GLOBALS['xmlrpcstr']['server_error']='Internal server error'; + $GLOBALS['xmlrpcerr']['multicall_error']=18; + $GLOBALS['xmlrpcstr']['multicall_error']='Received from server invalid multicall response'; + + $GLOBALS['xmlrpcerr']['multicall_notstruct'] = 9; + $GLOBALS['xmlrpcstr']['multicall_notstruct'] = 'system.multicall expected struct'; + $GLOBALS['xmlrpcerr']['multicall_nomethod'] = 10; + $GLOBALS['xmlrpcstr']['multicall_nomethod'] = 'missing methodName'; + $GLOBALS['xmlrpcerr']['multicall_notstring'] = 11; + $GLOBALS['xmlrpcstr']['multicall_notstring'] = 'methodName is not a string'; + $GLOBALS['xmlrpcerr']['multicall_recursion'] = 12; + $GLOBALS['xmlrpcstr']['multicall_recursion'] = 'recursive system.multicall forbidden'; + $GLOBALS['xmlrpcerr']['multicall_noparams'] = 13; + $GLOBALS['xmlrpcstr']['multicall_noparams'] = 'missing params'; + $GLOBALS['xmlrpcerr']['multicall_notarray'] = 14; + $GLOBALS['xmlrpcstr']['multicall_notarray'] = 'params is not an array'; + + $GLOBALS['xmlrpcerr']['cannot_decompress']=103; + $GLOBALS['xmlrpcstr']['cannot_decompress']='Received from server compressed HTTP and cannot decompress'; + $GLOBALS['xmlrpcerr']['decompress_fail']=104; + $GLOBALS['xmlrpcstr']['decompress_fail']='Received from server invalid compressed HTTP'; + $GLOBALS['xmlrpcerr']['dechunk_fail']=105; + $GLOBALS['xmlrpcstr']['dechunk_fail']='Received from server invalid chunked HTTP'; + $GLOBALS['xmlrpcerr']['server_cannot_decompress']=106; + $GLOBALS['xmlrpcstr']['server_cannot_decompress']='Received from client compressed HTTP request and cannot decompress'; + $GLOBALS['xmlrpcerr']['server_decompress_fail']=107; + $GLOBALS['xmlrpcstr']['server_decompress_fail']='Received from client invalid compressed HTTP request'; + + // The charset encoding used by the server for received messages and + // by the client for received responses when received charset cannot be determined + // or is not supported + $GLOBALS['xmlrpc_defencoding']='UTF-8'; + // The encoding used internally by PHP. + // String values received as xml will be converted to this, and php strings will be converted to xml + + // as if having been coded with this + $GLOBALS['xmlrpc_internalencoding']='ISO-8859-1'; + + $GLOBALS['xmlrpcName']='XML-RPC for PHP'; + $GLOBALS['xmlrpcVersion']='2.0RC3'; + + // let user errors start at 800 + $GLOBALS['xmlrpcerruser']=800; + // let XML parse errors start at 100 + $GLOBALS['xmlrpcerrxml']=100; + + // formulate backslashes for escaping regexp + $GLOBALS['xmlrpc_backslash']=chr(92).chr(92); + + // used to store state during parsing + // quick explanation of components: + // ac - used to accumulate values + // isf - used to indicate a fault + // lv - used to indicate "looking for a value": implements + // the logic to allow values with no types to be strings + // params - used to store parameters in method calls + // method - used to store method name + // stack - array with genealogy of xml elements names: + // used to validate nesting of xmlrpc elements + + $GLOBALS['_xh']=null; + + /** + * Convert a string to the correct XML representation in a target charset + * To help correct communication of non-ascii chars inside strings, regardless + * of the charset used when sending requests, parsing them, sending responses + * and parsing responses, an option is to convert all non-ascii chars present in the message + * into their equivalent 'charset entity'. Charset entities enumerated this way + * are independent of the charset encoding used to transmit them, and all XML + * parsers are bound to understand them. + * Note that in the std case we are not sending a charset encoding mime type + * along with http headers, so we are bound by RFC 3023 to emit strict us-ascii. + * + * @todo do a bit of basic benchmarking (strtr vs. str_replace) + * @todo make usage of iconv() or recode_string() or mb_string() where available + */ + function xmlrpc_encode_entitites($data, $src_encoding='', $dest_encoding='') + { + if ($src_encoding == '') + { + // lame, but we know no better... + $src_encoding = $GLOBALS['xmlrpc_internalencoding']; + } + //if ($dest_encoding == '') + //{ + // // lame, but we know no better... + // $dest_encoding = 'US-ASCII'; + //} + switch(strtoupper($src_encoding.'_'.$dest_encoding)) + { + case 'ISO-8859-1_': + case 'ISO-8859-1_US-ASCII': + $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); + $escaped_data = str_replace($GLOBALS['xml_iso88591_Entities']['in'], $GLOBALS['xml_iso88591_Entities']['out'], $escaped_data); + break; + case 'ISO-8859-1_UTF-8': + $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); + $escaped_data = utf8_encode($escaped_data); + break; + case 'ISO-8859-1_ISO-8859-1': + case 'US-ASCII_US-ASCII': + case 'US-ASCII_UTF-8': + case 'US-ASCII_': + case 'US-ASCII_ISO-8859-1': + case 'UTF-8_UTF-8': + $escaped_data = str_replace(array('&', '"', "'", '<', '>'), array('&', '"', ''', '<', '>'), $data); + break; + case 'UTF-8_': + case 'UTF-8_US-ASCII': + case 'UTF-8_ISO-8859-1': + // NB: this will choke on invalid UTF-8, going most likely beyond EOF + $escaped_data = ""; + // be kind to users creating string xmlrpcvals out of different php types + $data = (string) $data; + $ns = strlen ($data); + for ($nn = 0; $nn < $ns; $nn++) + { + $ch = $data[$nn]; + $ii = ord($ch); +//1 7 0bbbbbbb (127) + if ($ii < 128) + { + /// @todo shall we replace this with a (suuposedly) faster str_replace? + switch($ii){ + case 34: + $escaped_data .= '"'; + break; + case 38: + $escaped_data .= '&'; + case 39: + $escaped_data .= '''; + break; + case 60: + $escaped_data .= '<'; + break; + case 62: + $escaped_data .= '>'; + break; + default: + $escaped_data .= $ch; + } // switch + } +//2 11 110bbbbb 10bbbbbb (2047) + else if ($ii>>5 == 6) + { + $b1 = ($ii & 31); + $ii = ord($data[$nn+1]); + $b2 = ($ii & 63); + $ii = ($b1 * 64) + $b2; + $ent = sprintf ("&#%d;", $ii); + $escaped_data .= $ent; + } +//3 16 1110bbbb 10bbbbbb 10bbbbbb + else if ($ii>>4 == 14) + { + $b1 = ($ii & 31); + $ii = ord($data[$nn+1]); + $b2 = ($ii & 63); + $ii = ord($data[$nn+2]); + $b3 = ($ii & 63); + $ii = ((($b1 * 64) + $b2) * 64) + $b3; + $ent = sprintf ("&#%d;", $ii); + $escaped_data .= $ent; + } +//4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb + else if ($ii>>3 == 30) + { + $b1 = ($ii & 31); + $ii = ord($data[$nn+1]); + $b2 = ($ii & 63); + $ii = ord($data[$nn+2]); + $b3 = ($ii & 63); + $ii = ord($data[$nn+3]); + $b4 = ($ii & 63); + $ii = ((((($b1 * 64) + $b2) * 64) + $b3) * 64) + $b4; + $ent = sprintf ("&#%d;", $ii); + $escaped_data .= $ent; + } + } + break; + default: + $escaped_data = ''; + error_log("Converting from $src_encoding to $dest_encoding: not supported..."); + } +// } // switch + return $escaped_data; + } + + function xmlrpc_se($parser, $name, $attrs) + { + // if invalid xmlrpc already detected, skip all processing + if ($GLOBALS['_xh']['isf'] < 2) + { + // check for correct element nesting + // top level element can only be of 2 types + if (count($GLOBALS['_xh']['stack']) == 0) + { + if ($name != 'METHODRESPONSE' && $name != 'METHODCALL') + { + $GLOBALS['_xh']['isf'] = 2; + $GLOBALS['_xh']['isf_reason'] = 'missing top level xmlrpc element'; + return; + } + } + else + { + // not top level element: see if parent is OK + $parent = end($GLOBALS['_xh']['stack']); + if (!array_key_exists($name, $GLOBALS['xmlrpc_valid_parents']) || !in_array($parent, $GLOBALS['xmlrpc_valid_parents'][$name])) + { + $GLOBALS['_xh']['isf'] = 2; + $GLOBALS['_xh']['isf_reason'] = "xmlrpc element $name cannot be child of $parent"; + return; + } + } + + switch($name) + { + case 'STRUCT': + case 'ARRAY': + // create an empty array to hold child values, and push it onto appropriate stack + $cur_val = array(); + $cur_val['values'] = array(); + $cur_val['type'] = $name; + // check for out-of-band information to rebuild php objs + // and in case it is found, save it + if (@isset($attrs['PHP_CLASS'])) + { + $cur_val['php_class'] = $attrs['PHP_CLASS']; + } + $GLOBALS['_xh']['valuestack'][] = $cur_val; + break; + case 'DATA': + case 'METHODCALL': + case 'METHODRESPONSE': + case 'PARAMS': + // valid elements that add little to processing + break; + case 'METHODNAME': + case 'NAME': + $GLOBALS['_xh']['ac']=''; + break; + case 'FAULT': + $GLOBALS['_xh']['isf']=1; + break; + case 'VALUE': + $GLOBALS['_xh']['vt']='value'; // indicator: no value found yet + $GLOBALS['_xh']['ac']=''; + $GLOBALS['_xh']['lv']=1; + $GLOBALS['_xh']['php_class']=null; + break; + case 'I4': + case 'INT': + case 'STRING': + case 'BOOLEAN': + case 'DOUBLE': + case 'DATETIME.ISO8601': + case 'BASE64': + if ($GLOBALS['_xh']['vt']!='value') + { + //two data elements inside a value: an error occurred! + $GLOBALS['_xh']['isf'] = 2; + $GLOBALS['_xh']['isf_reason'] = "$name element following a {$GLOBALS['_xh']['vt']} element inside a single value"; + return; + } + + $GLOBALS['_xh']['ac']=''; // reset the accumulator + break; + case 'MEMBER': + $GLOBALS['_xh']['valuestack'][count($GLOBALS['_xh']['valuestack'])-1]['name']=''; // set member name to null, in case we do not find in the xml later on + //$GLOBALS['_xh']['ac']=''; + // Drop trough intentionally + case 'PARAM': + // clear value type, so we can check later if no value has been passed for this param/member + $GLOBALS['_xh']['vt']=null; + break; + default: + /// INVALID ELEMENT: RAISE ISF so that it is later recognized!!! + $GLOBALS['_xh']['isf'] = 2; + $GLOBALS['_xh']['isf_reason'] = "found not-xmlrpc xml element $name"; + break; + } + + // Save current element name to stack, to validate nesting + $GLOBALS['_xh']['stack'][] = $name; + + if($name!='VALUE') + { + $GLOBALS['_xh']['lv']=0; + } + } + } + + function xmlrpc_ee($parser, $name, $rebuild_xmlrpcvals = true) + { + if ($GLOBALS['_xh']['isf'] < 2) + { + // push this element name from stack + // NB: if XML validates, correct opening/closing is guaranteed and + // we do not have to check for $name == $curr_elem. + // we also checked for proper nesting at start of elements... + $curr_elem = array_pop($GLOBALS['_xh']['stack']); + + switch($name) + { + case 'STRUCT': + case 'ARRAY': + // fetch out of stack array of values, and promote it to current value + $curr_val = array_pop($GLOBALS['_xh']['valuestack']); + $GLOBALS['_xh']['value'] = $curr_val['values']; + $GLOBALS['_xh']['vt']=strtolower($name); + if (isset($curr_val['php_class'])) + { + $GLOBALS['_xh']['php_class'] = $curr_val['php_class']; + } + break; + case 'NAME': + $GLOBALS['_xh']['valuestack'][count($GLOBALS['_xh']['valuestack'])-1]['name'] = $GLOBALS['_xh']['ac']; + break; + case 'BOOLEAN': + case 'I4': + case 'INT': + case 'STRING': + case 'DOUBLE': + case 'DATETIME.ISO8601': + case 'BASE64': + $GLOBALS['_xh']['vt']=strtolower($name); + if ($name=='STRING') + { + $GLOBALS['_xh']['value']=$GLOBALS['_xh']['ac']; + } + elseif ($name=='DATETIME.ISO8601') + { + /// @todo validate datetime values with a correct format mask? + $GLOBALS['_xh']['vt']=$GLOBALS['xmlrpcDateTime']; + $GLOBALS['_xh']['value']=$GLOBALS['_xh']['ac']; + } + elseif ($name=='BASE64') + { + /// @todo check for failure of base64 decoding / catch warnings + $GLOBALS['_xh']['value']=base64_decode($GLOBALS['_xh']['ac']); + } + elseif ($name=='BOOLEAN') + { + // special case here: we translate boolean 1 or 0 into PHP + // constants true or false + // NB: this simple checks helps a lot sanitizing input, ie no + // security problems around here + if ($GLOBALS['_xh']['ac']=='1') + { + $GLOBALS['_xh']['value']=true; + } + else + { + // log if receiveing something strange, even though we set the value to false anyway + if ($GLOBALS['_xh']['ac']!='0') + error_log('XML-RPC: invalid value received in BOOLEAN: '.$GLOBALS['_xh']['ac']); + $GLOBALS['_xh']['value']=false; + } + } + elseif ($name=='DOUBLE') + { + // we have a DOUBLE + // we must check that only 0123456789-. are characters here + if (!ereg("^[+-]?[eE0123456789 \\t.]+$", $GLOBALS['_xh']['ac'])) + { + /// @todo: find a better way of throwing an error + // than this! + error_log('XML-RPC: non numeric value received in DOUBLE: '.$GLOBALS['_xh']['ac']); + $GLOBALS['_xh']['value']='ERROR_NON_NUMERIC_FOUND'; + } + else + { + // it's ok, add it on + $GLOBALS['_xh']['value']=(double)$GLOBALS['_xh']['ac']; + } + } + else + { + // we have an I4/INT + // we must check that only 0123456789- are characters here + if (!ereg("^[+-]?[0123456789 \\t]+$", $GLOBALS['_xh']['ac'])) + { + /// @todo find a better way of throwing an error + // than this! + error_log('XML-RPC: non numeric value received in INT: '.$GLOBALS['_xh']['ac']); + $GLOBALS['_xh']['value']='ERROR_NON_NUMERIC_FOUND'; + } + else + { + // it's ok, add it on + $GLOBALS['_xh']['value']=(int)$GLOBALS['_xh']['ac']; + } + } + $GLOBALS['_xh']['ac']=''; // is this necessary? + $GLOBALS['_xh']['lv']=3; // indicate we've found a value + break; + case 'VALUE': + // This if() detects if no scalar was inside + if ($GLOBALS['_xh']['vt']=='value') + { + $GLOBALS['_xh']['value']=$GLOBALS['_xh']['ac']; + $GLOBALS['_xh']['vt']=$GLOBALS['xmlrpcString']; + } + + if ($rebuild_xmlrpcvals) + { + // build the xmlrpc val out of the data received, and substitute it + $temp =& new xmlrpcval($GLOBALS['_xh']['value'], $GLOBALS['_xh']['vt']); + // in case we got info about underlying php class, save it + // in the object we're rebuilding + if (isset($GLOBALS['_xh']['php_class'])) + $temp->_php_class = $GLOBALS['_xh']['php_class']; + // check if we are inside an array or struct: + // if value just built is inside an array, let's move it into array on the stack + $vscount = count($GLOBALS['_xh']['valuestack']); + if ($vscount && $GLOBALS['_xh']['valuestack'][$vscount-1]['type']=='ARRAY') + { + $GLOBALS['_xh']['valuestack'][$vscount-1]['values'][] = $temp; + } + else + { + $GLOBALS['_xh']['value'] = $temp; + } + } + else + { + /// @todo this needs to treat correctly php-serialized objects, + /// since std deserializing is done by php_xmlrpc_decode, + /// which we will not be calling... + if (isset($GLOBALS['_xh']['php_class'])) + { + } + + // check if we are inside an array or struct: + // if value just built is inside an array, let's move it into array on the stack + $vscount = count($GLOBALS['_xh']['valuestack']); + if ($vscount && $GLOBALS['_xh']['valuestack'][$vscount-1]['type']=='ARRAY') + { + $GLOBALS['_xh']['valuestack'][$vscount-1]['values'][] = $GLOBALS['_xh']['value']; + } + } + break; + case 'MEMBER': + $GLOBALS['_xh']['ac']=''; // is this necessary? + // add to array in the stack the last element built, + // unless no VALUE was found + if ($GLOBALS['_xh']['vt']) + { + $vscount = count($GLOBALS['_xh']['valuestack']); + $GLOBALS['_xh']['valuestack'][$vscount-1]['values'][$GLOBALS['_xh']['valuestack'][$vscount-1]['name']] = $GLOBALS['_xh']['value']; + } else + error_log('XML-RPC: missing VALUE inside STRUCT in received xml'); + break; + case 'DATA': + $GLOBALS['_xh']['ac']=''; // is this necessary? + break; + case 'PARAM': + // add to array of params the current value, + // unless no VALUE was found + if ($GLOBALS['_xh']['vt']) + { + $GLOBALS['_xh']['params'][]=$GLOBALS['_xh']['value']; + $GLOBALS['_xh']['pt'][]=$GLOBALS['_xh']['vt']; + } + else + error_log('XML-RPC: missing VALUE inside PARAM in received xml'); + break; + case 'METHODNAME': + $GLOBALS['_xh']['method']=ereg_replace("^[\n\r\t ]+", '', $GLOBALS['_xh']['ac']); + break; + case 'PARAMS': + case 'FAULT': + case 'METHODCALL': + case 'METHORESPONSE': + break; + default: + // End of INVALID ELEMENT! + // shall we add an assert here for unreachable code??? + break; + } + } + } + + function xmlrpc_ee_fast($parser, $name) + { + xmlrpc_ee($parser, $name, false); + } + + function xmlrpc_cd($parser, $data) + { + //if(ereg("^[\n\r \t]+$", $data)) return; + // print "adding [${data}]\n"; + + // skip processing if xml fault already detected + if ($GLOBALS['_xh']['isf'] < 2) + { + if($GLOBALS['_xh']['lv']!=3) + { + // "lookforvalue==3" means that we've found an entire value + // and should discard any further character data + if($GLOBALS['_xh']['lv']==1) + { + // if we've found text and we're just in a then + // say we've found a value + $GLOBALS['_xh']['lv']=2; + } + if(!@isset($GLOBALS['_xh']['ac'])) + { + $GLOBALS['_xh']['ac'] = ''; + } + $GLOBALS['_xh']['ac'].=$data; + } + } + } + + function xmlrpc_dh($parser, $data) + { + // skip processing if xml fault already detected + if ($GLOBALS['_xh']['isf'] < 2) + { + if(substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') + { + if($GLOBALS['_xh']['lv']==1) + { + $GLOBALS['_xh']['lv']=2; + } + $GLOBALS['_xh']['ac'].=$data; + } + } + } + + class xmlrpc_client + { + var $path; + var $server; + var $port=0; + var $method='http'; + var $errno; + var $errstr; + var $debug=0; + var $username=''; + var $password=''; + var $cert=''; + var $certpass=''; + var $key=''; + var $keypass=''; + var $verifypeer=1; + var $verifyhost=1; + var $no_multicall=false; + var $proxy = ''; + var $proxyport=0; + var $proxy_user = ''; + var $proxy_pass = ''; + var $cookies=array(); + /** + * List of http compression methods accepted by the client for responses. + * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib + * + * NNB: you can set it to any non-empty array for HTTP11 and HTTPS, since + * in those cases it will be up to CURL to decide the compression methods + * it supports. You might check for the presence of 'zlib' in the output of + * curl_version() to determine wheter compression is supported or not + */ + var $accepted_compression = array(); + /** + * Name of compression scheme to be used for sending requests. + * Either null, gzip or deflate + */ + var $request_compression = ''; + /** + * CURL handle: used for keep-alive connections (PHP 4.3.8 up, see: + * http://curl.haxx.se/docs/faq.html#7.3) + */ + var $xmlrpc_curl_handle = null; + /// Wheter to use persistent connections for http 1.1 and https + var $keepalive = false; + /// Charset encodings that can be decoded without problems by the client + var $accepted_charset_encodings = array(); + /// Charset encoding to be used in serializing request. NULL = use ASCII + var $request_charset_encoding = ''; + /** + * Decides the content of xmlrpcresp objects returned by calls to send() + * valid strings are 'xmlrpcvals', 'phpvals' or 'xml' + */ + var $return_type = 'xmlrpcvals'; + + /** + * @param string $path either the complete server URL or the PATH part of the xmlrc server URL, e.g. /xmlrpc/server.php + * @param string $server the server name / ip address + * @param integer $port the port the server is listening on, defaults to 80 or 443 depending on protocol used + * @param string $method the http protocol variant: defaults to 'http', 'https' and 'http11' can be used if CURL is installed + */ + function xmlrpc_client($path, $server='', $port='', $method='') + { + // allow user to specify all params in $path + if($server == '' and $port == '' and $method == '') + { + $parts = parse_url($path); + $server = $parts['host']; + $path = $parts['path']; + if(isset($parts['query'])) + { + $path .= '?'.$parts['query']; + } + if(isset($parts['fragment'])) + { + $path .= '#'.$parts['fragment']; + } + if(isset($parts['port'])) + { + $port = $parts['port']; + } + if(isset($parts['scheme'])) + { + $method = $parts['scheme']; + } + if(isset($parts['user'])) + { + $this->username = $parts['user']; + } + if(isset($parts['pass'])) + { + $this->password = $parts['pass']; + } + } + if($path == '' || $path[0] != '/') + { + $this->path='/'.$path; + } + else + { + $this->path=$path; + } + $this->server=$server; + if($port != '') + { + $this->port=$port; + } + if($method != '') + { + $this->method=$method; + } + + // if ZLIB is enabled, let the client by default accept compressed responses + if(function_exists('gzinflate') || ( + function_exists('curl_init') && (($info = curl_version()) && + ((is_string($info) && strpos($info, 'zlib') !== null) || isset($info['libz_version']))) + )) + { + $this->accepted_compression = array('gzip', 'deflate'); + } + + // keepalives: enabled by default ONLY for PHP >= 4.3.8 + // (see http://curl.haxx.se/docs/faq.html#7.3) + if(version_compare(phpversion(), '4.3.8') >= 0) + { + $this->keepalive = true; + } + + // by default the xml parser can support these 3 charset encodings + $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII'); + } + + /* + * Enables/disables the echoing to screen of the xmlrpc responses received + * @param integer $debug values 0, 1 and 2 are supported (2 = echo sent msg too, beside received response) + * @access public + */ + function setDebug($in) + { + $this->debug=$in; + } + + /* + * Add some http BASIC AUTH credentials, used by the client to authenticate + * @param string $u username + * @param string $p password + * @access public + */ + function setCredentials($u, $p) + { + $this->username=$u; + $this->password=$p; + } + + /* + * Add a client-side https certificate + * @param string $cert + * @param string $certpass + * @access public + */ + function setCertificate($cert, $certpass) + { + $this->cert = $cert; + $this->certpass = $certpass; + } + + /* + * @param string $key The name of a file containing a private SSL key + * @param string $keypass The secret password needed to use the private SSL key + * @access public + * NB: does not work in older php/curl installs + * Thanks to Daniel Convissor + */ + function setKey($key, $keypass) + { + $this->key = $key; + $this->keypass = $keypass; + } + + /* + * @access public + */ + function setSSLVerifyPeer($i) + { + $this->verifypeer = $i; + } + + /* + * @access public + */ + function setSSLVerifyHost($i) + { + $this->verifyhost = $i; + } + + /** + * Set proxy info + * + * @param string $proxyhost + * @param string $proxyport Defaults to 8080 for HTTP and 443 for HTTPS + * @param string $proxyusername Leave blank if proxy has public access + * @param string $proxypassword Leave blank if proxy has public access + * @access public + */ + function setProxy($proxyhost, $proxyport, $proxyusername = '', $proxypassword = '') + { + $this->proxy = $proxyhost; + $this->proxyport = $proxyport; + $this->proxy_user = $proxyusername; + $this->proxy_pass = $proxypassword; + } + + /** + * Enables/disables reception of compressed xmlrpc responses. + * Note that enabling reception of compressed responses merely adds some standard + * http headers to xmlrpc requests. It is up to the xmlrpc server to return + * compressed responses when receiving such requests. + * @param string $compmethod either 'gzip', 'deflate', 'any' or '' + * @access public + */ + function setAcceptedCompression($compmethod) + { + if ($compmethod == 'any') + $this->accepted_compression = array('gzip', 'deflate'); + else + $this->accepted_compression = array($compmethod); + } + + /** + * Enables/disables http compression of xmlrpc request. + * Take care when sending compressed requests: servers might not support them + * (and automatic fallback to uncompressed requests is not yet implemented) + * @param string $compmethod either 'gzip', 'deflate' or '' + * @access public + */ + function setRequestCompression($compmethod) + { + $this->request_compression = $compmethod; + } + + /** + * Adds a cookie to list of cookies that will be sent to server. + * NB: setting any param but name and value will turn the cookie into a 'version 1' cookie: + * do not do it unless you know what you are doing + * @param string $name + * @param string $value + * @param string $path + * @param string $domain + * @param string $port + * @access public + * + * @todo check correctness of urlencoding cookie value (copied from php way of doing it...) + */ + function setCookie($name, $value='', $path='', $domain='', $port=null) + { + $this->cookies[$name]['value'] = urlencode($value); + if ($path || $domain || $port) + { + $this->cookies[$name]['path'] = $path; + $this->cookies[$name]['domain'] = $domain; + $this->cookies[$name]['port'] = $port; + $this->cookies[$name]['version'] = 1; + } + else + { + $this->cookies[$name]['version'] = 0; + } + } + + /** + * Send an xmlrpc request + * @param mixed $msg The message object, or an array of messages for using multicall, or the complete xml representation of a request + * @param integer $timeout Connection timeout, in seconds, If unspecified, a platform specific timeout will apply + * @param string $method if left unspecified, the http protocol chosen during creation of the object will be used + */ + function& send($msg, $timeout=0, $method='') + { + // if user deos not specify http protocol, use native method of this client + // (i.e. method set during call to constructor) + if($method == '') + { + $method = $this->method; + } + + if(is_array($msg)) + { + // $msg is an array of xmlrpcmsg's + $r = $this->multicall($msg, $timeout, $method); + return $r; + } + elseif(is_string($msg)) + { + $n =& new xmlrpcmsg(''); + $n->payload = $msg; + $msg = $n; + } + + // where msg is an xmlrpcmsg + $msg->debug=$this->debug; + + if($method == 'https') + { + $r =& $this->sendPayloadHTTPS( + $msg, + $this->server, + $this->port, + $timeout, + $this->username, + $this->password, + $this->cert, + $this->certpass, + $this->proxy, + $this->proxyport, + $this->proxy_user, + $this->proxy_pass, + $this->keepalive, + $this->key, + $this->keypass + ); + } + elseif($method == 'http11') + { + $r =& $this->sendPayloadCURL( + $msg, + $this->server, + $this->port, + $timeout, + $this->username, + $this->password, + null, + null, + $this->proxy, + $this->proxyport, + $this->proxy_user, + $this->proxy_pass, + 'http', + $this->keepalive + ); + } + else + { + $r =& $this->sendPayloadHTTP10( + $msg, + $this->server, + $this->port, + $timeout, + $this->username, + $this->password, + $this->proxy, + $this->proxyport, + $this->proxy_user, + $this->proxy_pass + ); + } + + return $r; + } + + /** + * @access private + */ + function &sendPayloadHTTP10($msg, $server, $port, $timeout=0,$username='', $password='', + $proxyhost='', $proxyport=0, $proxyusername='', $proxypassword='') + { + if($port==0) + { + $port=80; + } + + // Only create the payload if it was not created previously + if(empty($msg->payload)) + { + $msg->createPayload($this->request_charset_encoding); + } + + $payload = $msg->payload; + // Deflate request body and set appropriate request headers + if(function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) + { + if($this->request_compression == 'gzip') + { + $a = @gzencode($msg->payload); + if($a) + { + $payload = $a; + $encoding_hdr = "Content-Encoding: gzip\r\n"; + } + } + else + { + $a = @gzdeflate($msg->payload); + if($a) + { + $payload = $a; + $encoding_hdr = "Content-Encoding: deflate\r\n"; + } + } + } + else + { + $encoding_hdr = ''; + } + + // thanks to Grant Rauscher + // for this + $credentials=''; + if($username!='') + { + $credentials='Authorization: Basic ' . base64_encode($username . ':' . $password) . "\r\n"; + } + + $accepted_encoding = ''; + if(is_array($this->accepted_compression) && count($this->accepted_compression)) + { + $accepted_encoding = 'Accept-Encoding: ' . implode(', ', $this->accepted_compression) . "\r\n"; + } + + $proxy_credentials = ''; + if($proxyhost) + { + if($proxyport == 0) + { + $proxyport = 8080; + } + $connectserver = $proxyhost; + $connectport = $proxyport; + $uri = 'http://'.$server.':'.$port.$this->path; + if($proxyusername != '') + { + $proxy_credentials = 'Proxy-Authorization: Basic ' . base64_encode($proxyusername.':'.$proxypassword) . "\r\n"; + } + } + else + { + $connectserver = $server; + $connectport = $port; + $uri = $this->path; + } + + // Cookie generation, as per rfc2965 (version 1 cookies) or + // netscape's rules (version 0 cookies) + $cookieheader=''; + foreach ($this->cookies as $name => $cookie) + { + if ($cookie['version']) + { + $cookieheader .= 'Cookie: $Version="' . $cookie['version'] . '"; '; + $cookieheader .= $name . '="' . $cookie['value'] . '";'; + if ($cookie['path']) + $cookieheader .= ' $Path="' . $cookie['path'] . '";'; + if ($cookie['domain']) + $cookieheader .= ' $Domain="' . $cookie['domain'] . '";'; + if ($cookie['port']) + $cookieheader .= ' $Port="' . $cookie['domain'] . '";'; + $cookieheader = substr($cookieheader, 0, -1) . "\r\n"; + } + else + { + $cookieheader .= 'Cookie: ' . $name . '=' . $cookie['value'] . "\r\n"; + } + } + + $op= "POST " . $uri. " HTTP/1.0\r\n" . + "User-Agent: " . $GLOBALS['xmlrpcName'] . " " . $GLOBALS['xmlrpcVersion'] . "\r\n" . + "Host: ". $server . "\r\n" . + $credentials . + $proxy_credentials . + $accepted_encoding . + $encoding_hdr . + "Accept-Charset: " . implode(',', $this->accepted_charset_encodings) . "\r\n" . + $cookieheader . + "Content-Type: " . $msg->content_type . "\r\nContent-Length: " . + strlen($payload) . "\r\n\r\n" . + $payload; + + + if($this->debug > 1) + { + print "
\n---SENDING---\n" . htmlentities($op) . "\n---END---\n
"; + // let the client see this now in case http times out... + flush(); + } + + if($timeout>0) + { + $fp=@fsockopen($connectserver, $connectport, $this->errno, $this->errstr, $timeout); + } + else + { + $fp=@fsockopen($connectserver, $connectport, $this->errno, $this->errstr); + } + if($fp) + { + if($timeout>0 && function_exists('stream_set_timeout')) + { + stream_set_timeout($fp, $timeout); + } + } + else + { + $this->errstr='Connect error: '.$this->errstr; + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $this->errstr . ' (' . $this->errno . ')'); + return $r; + } + + if(!fputs($fp, $op, strlen($op))) + { + $this->errstr='Write error'; + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $this->errstr); + return $r; + } + else + { + // reset errno and errstr on succesful socket connection + $this->errstr = ''; + } + // G. Giunta 2005/10/24: close socket before parsing. + // should yeld slightly better execution times, and make easier recursive calls (e.g. to follow http redirects) + //$resp=&$msg->parseResponseFile($fp); + $ipd=''; + while($data=fread($fp, 32768)) + { + // shall we check for $data === FALSE? + // as per the manual, it signals an error + $ipd.=$data; + } + fclose($fp); + $r =& $msg->parseResponse($ipd, false, $this->return_type); + return $r; + + } + + /** + * @access private + */ + function &sendPayloadHTTPS($msg, $server, $port, $timeout=0,$username='', $password='', $cert='',$certpass='', + $proxyhost='', $proxyport=0, $proxyusername='', $proxypassword='', $keepalive=false, $key='', $keypass='') + { + $r =& $this->sendPayloadCURL($msg, $server, $port, $timeout, $username, $password, $cert, $certpass, + $proxyhost, $proxyport, $proxyusername, $proxypassword, 'https', $keepalive, $key, $keypass); + return $r; + } + + /** + * Contributed by Justin Miller + * Requires curl to be built into PHP + * NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers! + * @access private + */ + function &sendPayloadCURL($msg, $server, $port, $timeout=0, $username='', $password='', $cert='', $certpass='', + $proxyhost='', $proxyport=0, $proxyusername='', $proxypassword='', $method='https', $keepalive=false, + $key='', $keypass='') + { + if(!function_exists('curl_init')) + { + $this->errstr='CURL unavailable on this install'; + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['no_curl'], $GLOBALS['xmlrpcstr']['no_curl']); + return $r; + } + if($method == 'https') + { + if(($info = curl_version()) && + ((is_string($info) && strpos($info, 'OpenSSL') === null) || (is_array($info) && !isset($info['ssl_version'])))) + { + $this->errstr='SSL unavailable on this install'; + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['no_ssl'], $GLOBALS['xmlrpcstr']['no_ssl']); + return $r; + } + } + + if($port == 0) + { + if($method == 'http') + { + $port = 80; + } + else + { + $port = 443; + } + } + + // Only create the payload if it was not created previously + if(empty($msg->payload)) + { + $msg->createPayload($this->request_charset_encoding); + } + + // Deflate request body and set appropriate request headers + $payload = $msg->payload; + if(function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) + { + if($this->request_compression == 'gzip') + { + $a = @gzencode($msg->payload); + if($a) + { + $payload = $a; + $encoding_hdr = "Content-Encoding: gzip"; + } + } + else + { + $a = @gzdeflate($msg->payload); + if($a) + { + $payload = $a; + $encoding_hdr = "Content-Encoding: deflate"; + } + } + } + else + { + $encoding_hdr = ''; + } + + if($this->debug > 1) + { + print "
\n---SENDING---\n" . htmlentities($payload) . "\n---END---\n
"; + // let the client see this now in case http times out... + flush(); + } + + if(!$keepalive || !$this->xmlrpc_curl_handle) + { + $curl = curl_init($method . '://' . $server . ':' . $port . $this->path); + if($keepalive) + { + $this->xmlrpc_curl_handle = $curl; + } + } + else + { + $curl = $this->xmlrpc_curl_handle; + } + + // results into variable + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + + if($this->debug) + { + curl_setopt($curl, CURLOPT_VERBOSE, 1); + } + curl_setopt($curl, CURLOPT_USERAGENT, $GLOBALS['xmlrpcName'].' '.$GLOBALS['xmlrpcVersion']); + // required for XMLRPC: post the data + curl_setopt($curl, CURLOPT_POST, 1); + // the data + curl_setopt($curl, CURLOPT_POSTFIELDS, $payload); + + // return the header too + curl_setopt($curl, CURLOPT_HEADER, 1); + + // will only work with PHP >= 5.0 + // NB: if we set an empty string, CURL will add http header indicating + // ALL methods it is supporting. This is possibly a better option than + // letting the user tell what curl can / cannot do... + if(is_array($this->accepted_compression) && count($this->accepted_compression)) + { + //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $this->accepted_compression)); + // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?) + curl_setopt($curl, CURLOPT_ENCODING, ''); + } + // extra headers + $headers = array('Content-Type: ' . $msg->content_type , 'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings)); + // if no keepalive is wanted, let the server know it in advance + if(!$keepalive) + { + $headers[] = 'Connection: close'; + } + // request compression header + if($encoding_hdr) + { + $headers[] = $encoding_hdr; + } + + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + // timeout is borked + if($timeout) + { + curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1); + } + + if($username && $password) + { + curl_setopt($curl, CURLOPT_USERPWD,"$username:$password"); + } + + if($method == 'https') + { + // set cert file + if($cert) + { + curl_setopt($curl, CURLOPT_SSLCERT, $cert); + } + // set cert password + if($certpass) + { + curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $certpass); + } + // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?) + if($key) + { + curl_setopt($curl, CURLOPT_SSLKEY, $key); + } + // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?) + if($keypass) + { + curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $keypass); + } + // whether to verify remote host's cert + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifypeer); + // whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that it matches the hostname used + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->verifyhost); + } + + // proxy info + if($proxyhost) + { + if($proxyport == 0) + { + $proxyport = 8080; // NB: even for HTTPS, local connection is on port 8080 + } + curl_setopt($curl, CURLOPT_PROXY,$proxyhost.':'.$proxyport); + //curl_setopt($curl, CURLOPT_PROXYPORT,$proxyport); + if($proxyusername) + { + curl_setopt($curl, CURLOPT_PROXYUSERPWD, $proxyusername.':'.$proxypassword); + } + } + + // NB: should we build cookie http headers by hand rather than let CURL do it? + // the following code does not honour 'expires', 'path' and 'domain' cookie attributes + // set to clint obj the the user... + if (count($this->cookies)) + { + $cookieheader = ''; + foreach ($this->cookies as $name => $cookie) + { + $cookieheader .= $name . '=' . $cookie['value'] . ', '; + } + curl_setopt($curl, CURLOPT_COOKIE, substr($cookieheader, 0, -2)); + } + + $result = curl_exec($curl); + + if(!$result) + { + $this->errstr='no response'; + $resp=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['curl_fail'], $GLOBALS['xmlrpcstr']['curl_fail']. ': '. curl_error($curl)); + if(!$keepalive) + { + curl_close($curl); + } + } + else + { + if(!$keepalive) + { + curl_close($curl); + } + $resp =& $msg->parseResponse($result, true, $this->return_type); + } + return $resp; + } + + /** + * Send an array of request messages and return an array of responses. + * Unless $this->no_multicall has been set to true, it will try first + * to use one single xmlrpc call to server method system.multicall, and + * revert to sending many successive calls in case of failure. + * This failure is also stored in $this->no_multicall for subsequent calls. + * Unfortunately, there is no server error code universally used to denote + * the fact that multicall is unsupported, so there is no way to reliably + * distinguish between that and a temporary failure. + * If you are sure that server supports multicall and do not want to + * fallback to using many single calls, set the fourth parameter to FALSE. + * + * NB: trying to shoehorn extra functionality into existing syntax has resulted + * in pretty much convoluted code... + * + * @access public + * @param array $msgs an array of xmlrpcmsg objects + * @param integer $timeout connection timeout (in seconds) + * @param string $method the http protocol variant to be used + * @param boolen fallback When true, upon receiveing an error during multicall, multiple single calls will be attempted + */ + function multicall($msgs, $timeout=0, $method='http', $fallback=true) + { + if(!$this->no_multicall) + { + $results = $this->_try_multicall($msgs, $timeout, $method); + if(is_array($results)) + { + // System.multicall succeeded + return $results; + } + else + { + // either system.multicall is unsupported by server, + // or call failed for some other reason. + if ($fallback) + { + // Don't try it next time... + $this->no_multicall = true; + } + else + { + if (is_a($result, 'xmlrpcresp')) + { + $result = $results; + } + else + { + $result =& new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['multicall_error'], $GLOBALS['xmlrpcstr']['multicall_error']); + } + } + } + } + else + { + // override fallback, in case careless user tries to do two + // opposite things at the same time + $fallback = true; + } + + $results = array(); + if ($fallback) + { + // system.multicall is (probably) unsupported by server: + // emulate multicall via multiple requests + foreach($msgs as $msg) + { + $results[] =& $this->send($msg, $timeout, $method); + } + } + else + { + // user does NOT want to fallback on many single calls: + // since we should always return an array of responses, + // return an array with the same error repeated n times + foreach($msgs as $msg) + { + $results[] = $result; + } + } + return $results; + } + + /** + * Attempt to boxcar $msgs via system.multicall. + * Returns either an array of xmlrpcreponses, an xmlrpc error response + * or false (when recived response does not respect valid multiccall syntax) + * @access private + */ + function _try_multicall($msgs, $timeout, $method) + { + // Construct multicall message + $calls = array(); + foreach($msgs as $msg) + { + $call['methodName'] =& new xmlrpcval($msg->method(),'string'); + $numParams = $msg->getNumParams(); + $params = array(); + for($i = 0; $i < $numParams; $i++) + { + $params[$i] = $msg->getParam($i); + } + $call['params'] =& new xmlrpcval($params, 'array'); + $calls[] =& new xmlrpcval($call, 'struct'); + } + $multicall =& new xmlrpcmsg('system.multicall'); + $multicall->addParam(new xmlrpcval($calls, 'array')); + + // Attempt RPC call + $result =& $this->send($multicall, $timeout, $method); + //if(!is_object($result)) + //{ + // return ($result || 0); // transport failed + //} + + if($result->faultCode() != 0) + { + // call to system.multicall failed + return $result; + } + + // Unpack responses. + $rets = $result->value(); + + if ($this->return_type == 'xml') + { + return $rets; + } + else if ($this->return_type == 'phpvals') + { + ///@todo test this code branch... + $rets = $result->value(); + if(!is_array($rets)) + { + return false; // bad return type from system.multicall + } + $numRets = count($rets); + if($numRets != count($msgs)) + { + return false; // wrong number of return values. + } + + $response = array(); + for($i = 0; $i < $numRets; $i++) + { + $val = $rets[$i]; + if (!is_array($val)) { + return false; + } + switch(count($val)) + { + case 1: + if(!isset($val[0])) + { + return false; // Bad value + } + // Normal return value + $response[$i] =& new xmlrpcresp($val[0], 0, '', 'phpvals'); + break; + case 2: + /// @todo remove usage of @: it is apparently quite slow + $code = @$val['faultCode']; + if(!is_int($code)) + { + return false; + } + $str = @$val['faultString']; + if(!is_string($str)) + { + return false; + } + $response[$i] =& new xmlrpcresp(0, $code, $str); + break; + default: + return false; + } + } + return $response; + } + else // return type == 'xmlrpcvals' + { + $rets = $result->value(); + if($rets->kindOf() != 'array') + { + return false; // bad return type from system.multicall + } + $numRets = $rets->arraysize(); + if($numRets != count($msgs)) + { + return false; // wrong number of return values. + } + + $response = array(); + for($i = 0; $i < $numRets; $i++) + { + $val = $rets->arraymem($i); + switch($val->kindOf()) + { + case 'array': + if($val->arraysize() != 1) + { + return false; // Bad value + } + // Normal return value + $response[$i] =& new xmlrpcresp($val->arraymem(0)); + break; + case 'struct': + $code = $val->structmem('faultCode'); + if($code->kindOf() != 'scalar' || $code->scalartyp() != 'int') + { + return false; + } + $str = $val->structmem('faultString'); + if($str->kindOf() != 'scalar' || $str->scalartyp() != 'string') + { + return false; + } + $response[$i] =& new xmlrpcresp(0, $code->scalarval(), $str->scalarval()); + break; + default: + return false; + } + } + return $response; + } + } + } // end class xmlrpc_client + + class xmlrpcresp + { + var $val = 0; + var $valtyp; + var $errno = 0; + var $errstr = ''; + var $hdrs = array(); + var $_cookies = array(); + var $content_type = 'text/xml'; + + /** + * @param mixed $val either an xmlrpcval obj, a php value or the xml serialization of an xmlrpcval (a string) + * @param integer $fcode set it to anything but 0 to create an error response + * @param string $fstr the error string, in case of an error response + * @param string $valtyp either 'xmlrpcvals', 'phpvals' or 'xml' + * + * @todo add check that $val is of correct type??? + * NB: as of now we do not do it, since it might be either an xmlrpcval or a plain + * php val, or a complete xml chunk, depending on usage of xmlrpc_client::send() inside which creator is called... + */ + function xmlrpcresp($val, $fcode = 0, $fstr = '', $valtyp='') + { + if($fcode != 0) + { + // error response + $this->errno = $fcode; + $this->errstr = $fstr; + //$this->errstr = htmlspecialchars($fstr); // XXX: encoding probably shouldn't be done here; fix later. + } + /*elseif(!is_object($val) || !is_a($val, 'xmlrpcval')) + { + // programmer error + error_log("Invalid type '" . gettype($val) . "' (value: $val) passed to xmlrpcresp. Defaulting to empty value."); + $this->val =& new xmlrpcval(); + }*/ + else + { + // successful response + $this->val = $val; + if ($valtyp == '') + { + // user did not declare type of response value: try to guess it + if (is_object($this->val) && is_a($this->val, 'xmlrpcval')) + { + $this->valtyp = 'xmlrpcvals'; + } + else if (is_string($this->val)) + { + $this->valtyp = 'xml'; + + } + else + { + $this->valtyp = 'phpvals'; + } + } + else + { + // user declares type of resp value: believe him + $this->valtyp = $valtyp; + } + } + } + + /* + * @return integer the error code of this response (0 for not-error responses) + */ + function faultCode() + { + return $this->errno; + } + + /* + * @return string the error string of this response ('' for not-error responses) + */ + function faultString() + { + return $this->errstr; + } + + /* + * @return mixed the xmlrpcval object returned by the server. Might be an xml string or php value if the response has been created by specially configured xmlrpc_client objects + */ + function value() + { + return $this->val; + } + + /** + * Returns an array with the cookies received from the server. + * Array has the form: $cookiename => array ('value' => $val, $attr1 => $val1, $attr2 = $val2, ...) + * with attributes being e.g. 'expires', 'path', domain'. + * NB: cookies sent as 'expired' by the server (i.e. with an expiry date in the past) + * are still present in the array. It is up to the user-defined code to decide + * how to use the received cookies, and wheter they have to be sent back with the next + * request to the server (using xmlrpc_client::setCookie) or not + * @return array array of cookies received from the server + * @access public + */ + function cookies() + { + return $this->_cookies; + } + + /** + * Return xml representation of the response + * @param string $charset_encoding the charset to be used for serialization. if null, US-ASCII is assumed + * @return string the xml representation of the response + */ + function serialize($charset_encoding='') + { + if ($charset_encoding != '') + $this->content_type = 'text/xml; charset=' . $charset_encoding; + else + $this->content_type = 'text/xml'; + $result = "\n"; + if($this->errno) + { + // G. Giunta 2005/2/13: let non-ASCII response messages be tolerated by clients + // by xml-encoding non ascii chars + $result .= "\n" . +"\nfaultCode\n" . $this->errno . +"\n\n\nfaultString\n" . +xmlrpc_encode_entitites($this->errstr, $GLOBALS['xmlrpc_internalencoding'], $charset_encoding) . "\n\n" . +"\n\n"; + } + else + { + if(!is_object($this->val) || !is_a($this->val, 'xmlrpcval')) + { + if (is_string($this->val) && $this->valtyp == 'xml') + { + $result .= "\n\n" . + $this->val . + "\n"; + } + else + { + /// @todo try to build something serializable? + die('cannot serialize xmlrpcresp objects whose content is native php values'); + } + } + else + { + $result .= "\n\n" . + $this->val->serialize($charset_encoding) . + "\n"; + } + } + $result .= "\n"; + return $result; + } + } + + class xmlrpcmsg + { + var $payload; + var $methodname; + var $params=array(); + var $debug=0; + var $content_type = 'text/xml'; + + /* + * @param string $meth the name of the method to invoke + * @param array $pars array of parameters to be paased to the method (xmlrpcval objects) + */ + function xmlrpcmsg($meth, $pars=0) + { + $this->methodname=$meth; + if(is_array($pars) && sizeof($pars)>0) + { + for($i=0; $iaddParam($pars[$i]); + } + } + } + + function xml_header() + { + return "\n\n"; + } + + function xml_footer() + { + return ""; + } + + function kindOf() + { + return 'msg'; + } + + function createPayload($charset_encoding='') + { + if ($charset_encoding != '') + $this->content_type = 'text/xml; charset=' . $charset_encoding; + else + $this->content_type = 'text/xml'; + $this->payload=$this->xml_header(); + $this->payload.='' . $this->methodname . "\n"; + // if(sizeof($this->params)) { + $this->payload.="\n"; + for($i=0; $iparams); $i++) + { + $p=$this->params[$i]; + $this->payload.="\n" . $p->serialize($charset_encoding) . + "\n"; + } + $this->payload.="\n"; + // } + $this->payload.=$this->xml_footer(); + //$this->payload=str_replace("\n", "\r\n", $this->payload); + } + + /* + * Gets/sets the xmlrpc method to be invoked + * @param string $meth the method to be set (leave empty not to set it) + * @return string the method that will be invoked + * @access public + */ + function method($meth='') + { + if($meth!='') + { + $this->methodname=$meth; + } + return $this->methodname; + } + + /* + * @return string the xml representation of the message + */ + function serialize($charset_encoding='') + { + $this->createPayload($charset_encoding); + return $this->payload; + } + + /* + * Add a parameter to the list of parameters to be used upon method invocation + * @param xmlrpcval $par + * @return boolean false on failure + */ + function addParam($par) + { + // add check: do not add to self params which are not xmlrpcvals + if(is_object($par) && is_a($par, 'xmlrpcval')) + { + $this->params[]=$par; + return true; + } + else + { + return false; + } + } + + /* + * @param integer $i the index of the parameter to fetch (zero based) + * @return xmlrpcval the i-th parameter + */ + function getParam($i) { return $this->params[$i]; } + + /* + * @return integer the number of parameters currently set + */ + function getNumParams() { return sizeof($this->params); } + + /* + * @access private + * @todo add 2nd & 3rd param to be passed to ParseResponse() ??? + */ + function &parseResponseFile($fp) + { + $ipd=''; + while($data=fread($fp, 32768)) + { + $ipd.=$data; + } + //fclose($fp); + $r =& $this->parseResponse($ipd); + return $r; + } + + /** + * Parses HTTP headers and separates them from data. + * @access private + */ + function &parseResponseHeaders(&$data, $headers_processed=false) + { + // Strip HTTP 1.1 100 Continue header if present + while(ereg('^HTTP/1\.1 1[0-9]{2} ', $data)) + { + $pos = strpos($data, 'HTTP', 12); + // server sent a Continue header without any (valid) content following... + // give the client a chance to know it + if(!$pos && !is_int($pos)) // works fine in php 3, 4 and 5 + { + break; + } + $data = substr($data, $pos); + } + if(!ereg('^HTTP/[0-9.]+ 200 ', $data)) + { + $errstr= substr($data, 0, strpos($data, "\n")-1); + error_log('XML-RPC: xmlrpcmsg::parseResponse: HTTP error, got response: ' .$errstr); + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['http_error'], $GLOBALS['xmlrpcstr']['http_error']. ' (' . $errstr . ')'); + return $r; + } + + $GLOBALS['_xh']['headers'] = array(); + $GLOBALS['_xh']['cookies'] = array(); + + // be tolerant to usage of \n instead of \r\n to separate headers and data + // (even though it is not valid http) + $pos = strpos($data,"\r\n\r\n"); + if($pos || is_int($pos)) + { + $bd = $pos+4; + } + else + { + $pos = strpos($data,"\n\n"); + if($pos || is_int($pos)) + { + $bd = $pos+2; + } + else + { + // No separation between response headers and body: fault? + $bd = 0; + } + } + // be tolerant to line endings, and extra empty lines + $ar = split("\r?\n", trim(substr($data, 0, $pos))); + while(list(,$line) = @each($ar)) + { + // take care of multi-line headers and cookies + $arr = explode(':',$line,2); + if(count($arr) > 1) + { + $header_name = strtolower(trim($arr[0])); + /// @todo some other headers (the ones that allow a CSV list of values) + /// do allow many values to be passed using multiple header lines. + /// We should add content to $GLOBALS['_xh']['headers'][$header_name] + /// instead of replacing it for those... + if ($header_name == 'set-cookie' || $header_name == 'set-cookie2') + { + if ($header_name == 'set-cookie2') + { + // version 2 cookies: + // there could be many cookies on one line, comma separated + $cookies = explode(',', $arr[1]); + } + else + { + $cookies = array($arr[1]); + } + foreach ($cookies as $cookie) + { + // glue together all received cookies, using a comma to separate them + // (same as php does with getallheaders()) + if (isset($GLOBALS['_xh']['headers'][$header_name])) + $GLOBALS['_xh']['headers'][$header_name] .= ', ' . trim($cookie); + else + $GLOBALS['_xh']['headers'][$header_name] = trim($cookie); + // parse cookie attributes, in case user wants to coorectly honour then + // feature creep: only allow rfc-compliant cookie attributes? + $cookie = explode(';', $cookie); + foreach ($cookie as $pos => $val) + { + $val = explode('=', $val, 2); + $tag = trim($val[0]); + $val = trim(@$val[1]); + /// @todo with version 1 cookies, we should strip leading and trailing " chars + if ($pos == 0) + { + $cookiename = $tag; + $GLOBALS['_xh']['cookies'][$tag] = array(); + $GLOBALS['_xh']['cookies'][$cookiename]['value'] = urldecode($val); + } + else + { + $GLOBALS['_xh']['cookies'][$cookiename][$tag] = $val; + } + } + } + } + else + { + $GLOBALS['_xh']['headers'][$header_name] = trim($arr[1]); + } + } + elseif(isset($header_name)) + { + /// @todo version1 cookies might span multiple lines, thus breaking the parsing above + $GLOBALS['_xh']['headers'][$header_name] .= ' ' . trim($line); + } + } + // rebuild full cookie set + /*if (isset($GLOBALS['_xh']['headers']['set-cookie'])) + { + $cookies = array(); + $received = explode(';', $GLOBALS['_xh']['headers']['set-cookie']); + foreach($received as $cookie) + { + list($name, $value) = explode('=', $cookie); + $name = trim($name); + $value = trim($value); + // these values are in fact attributes + if ($name != 'Comment' && $name != 'Comment' && $name != 'Comment' && $name != 'Comment' && $name != 'Comment' && $name != 'Comment') + { + $cookies[$name] = $value; + } + } + }*/ + + $data = substr($data, $bd); + + if($this->debug && count($GLOBALS['_xh']['headers'])) + { + print '
';
+					foreach($GLOBALS['_xh']['headers'] as $header => $value)
+					{
+						print "HEADER: $header: $value\n";
+					}
+					foreach($GLOBALS['_xh']['cookies'] as $header => $value)
+					{
+						print "COOKIE: $header={$value['value']}\n";
+					}
+					print "
\n"; + } + + // if CURL was used for the call, http headers have been processed, + // and dechunking + reinflating have been carried out + if(!$headers_processed) + { + // Decode chunked encoding sent by http 1.1 servers + if(isset($GLOBALS['_xh']['headers']['transfer-encoding']) && $GLOBALS['_xh']['headers']['transfer-encoding'] == 'chunked') + { + if(!$data = decode_chunked($data)) + { + error_log('XML-RPC: xmlrpcmsg::parseResponse: errors occurred when trying to rebuild the chunked data received from server'); + $r =& new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['dechunk_fail'], $GLOBALS['xmlrpcstr']['dechunk_fail']); + return $r; + } + } + + // Decode gzip-compressed stuff + // code shamelessly inspired from nusoap library by Dietrich Ayala + if(isset($GLOBALS['_xh']['headers']['content-encoding'])) + { + if($GLOBALS['_xh']['headers']['content-encoding'] == 'deflate' || $GLOBALS['_xh']['headers']['content-encoding'] == 'gzip') + { + // if decoding works, use it. else assume data wasn't gzencoded + if(function_exists('gzinflate')) + { + if($GLOBALS['_xh']['headers']['content-encoding'] == 'deflate' && $degzdata = @gzinflate($data)) + { + $data = $degzdata; + if($this->debug) + print "
---INFLATED RESPONSE---[".strlen($data)." chars]---\n" . htmlentities($data) . "\n---END---
"; + } + elseif($GLOBALS['_xh']['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) + { + $data = $degzdata; + if($this->debug) + print "
---INFLATED RESPONSE---[".strlen($data)." chars]---\n" . htmlentities($data) . "\n---END---
"; + } + else + { + error_log('XML-RPC: xmlrpcmsg::parseResponse: errors occurred when trying to decode the deflated data received from server'); + $r =& new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['decompress_fail'], $GLOBALS['xmlrpcstr']['decompress_fail']); + return $r; + } + } + else + { + error_log('XML-RPC: xmlrpcmsg::parseResponse: the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.'); + $r =& new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['cannot_decompress'], $GLOBALS['xmlrpcstr']['cannot_decompress']); + return $r; + } + } + } + } // end of 'if needed, de-chunk, re-inflate response' + + // real stupid hack to avoid PHP 4 complaining about returning NULL by ref + $r = null; + $r =& $r; + return $r; + } + + /* + * @param string $data the xmlrpc response, eventually including http headers + * @param bool $headers_processed when true prevents parsing HTTP headers for interpretation of content-encoding and conseuqent decoding + * @param string $return_type decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or 'phpvals' + * @access private + */ + function &parseResponse($data='', $headers_processed=false, $return_type='xmlrpcvals') + { + //$hdrfnd = 0; + if($this->debug) + { + //by maHo, replaced htmlspecialchars with htmlentities + print "
---GOT---\n" . htmlentities($data) . "\n---END---\n
"; + $start = strpos($data, '', $start); + $comments = substr($data, $start, $end-$start); + print "
---SERVER DEBUG INFO (DECODED) ---\n\t".htmlentities(str_replace("\n", "\n\t", base64_decode($comments)))."\n---END---\n
"; + } + } + + if($data == '') + { + error_log('XML-RPC: xmlrpcmsg::parseResponse: no response received from server.'); + $r =& new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['no_data'], $GLOBALS['xmlrpcstr']['no_data']); + return $r; + } + + $GLOBALS['_xh']=array(); + + // parse the HTTP headers of the response, if present, and separate them from data + if(ereg("^HTTP",$data)) + { + $r =& $this->parseResponseHeaders($data, $headers_processed); + if ($r) + { + return $r; + } + } + else + { + $GLOBALS['_xh']['headers'] = array(); + $GLOBALS['_xh']['cookies'] = array(); + } + + + // be tolerant of extra whitespace in response body + $data = trim($data); + + /// @todo return an error msg if $data=='' ? + + // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts) + // idea from Luca Mariano originally in PEARified version of the lib + $bd = false; + // Poor man's version of strrpos for php 4... + $pos = strpos($data, ''); + while($pos || is_int($pos)) + { + $bd = $pos+17; + $pos = strpos($data, '', $bd); + } + if($bd) + { + $data = substr($data, 0, $bd); + } + + // if user wants back raw xml, give it to him + if ($return_type == 'xml') + { + $r =& new xmlrpcresp($data, 0, '', 'xml'); + $r->hdrs = $GLOBALS['_xh']['headers']; + $r->_cookies = $GLOBALS['_xh']['cookies']; + return $r; + } + + // try to 'guestimate' the character encoding of the received response + $resp_encoding = guess_encoding(@$GLOBALS['_xh']['headers']['content-type'], $data); + + $GLOBALS['_xh']['stack'] = array(); + $GLOBALS['_xh']['valuestack'] = array(); + $GLOBALS['_xh']['isf']=0; + $GLOBALS['_xh']['isf_reason']=''; + $GLOBALS['_xh']['ac']=''; + $GLOBALS['_xh']['qt']=''; + + // if response charset encoding is not known / supported, try to use + // the default encoding and parse the xml anyway, but log a warning... + if (!in_array($resp_encoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) + // the following code might be better for mb_string enabled installs, but + // makes the lib about 200% slower... + //if (!is_valid_charset($resp_encoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) + { + error_log('XML-RPC: xmlrpcmsg::parseResponse: invalid charset encoding of received response: '.$resp_encoding); + $resp_encoding = $GLOBALS['xmlrpc_defencoding']; + } + $parser = xml_parser_create($resp_encoding); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true); + // G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell + // the xml parser to give us back data in the expected charset + xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $GLOBALS['xmlrpc_internalencoding']); + + if ($return_type == 'phpvals') + { + xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast'); + } + else + { + xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee'); + } + + xml_set_character_data_handler($parser, 'xmlrpc_cd'); + xml_set_default_handler($parser, 'xmlrpc_dh'); + + if(!xml_parse($parser, $data, sizeof($data))) + { + // thanks to Peter Kocks + if((xml_get_current_line_number($parser)) == 1) + { + $errstr = 'XML error at line 1, check URL'; + } + else + { + $errstr = sprintf('XML error: %s at line %d', + xml_error_string(xml_get_error_code($parser)), + xml_get_current_line_number($parser)); + } + error_log($errstr); + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'], $GLOBALS['xmlrpcstr']['invalid_return'].' ('.$errstr.')'); + xml_parser_free($parser); + if($this->debug) + { + print $errstr; + } + $r->hdrs = $GLOBALS['_xh']['headers']; + $r->_cookies = $GLOBALS['_xh']['cookies']; + return $r; + } + xml_parser_free($parser); + if ($GLOBALS['_xh']['isf'] > 1) + { + if ($this->debug) + { + /// @todo echo something for user? + } + + $r =& new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'], + $GLOBALS['xmlrpcstr']['invalid_return'] . ' ' . $GLOBALS['_xh']['isf_reason']); + } + elseif ($return_type == 'xmlrpcvals' && !is_object($GLOBALS['_xh']['value'])) + { + // then something odd has happened + // and it's time to generate a client side error + // indicating something odd went on + $r=&new xmlrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'], + $GLOBALS['xmlrpcstr']['invalid_return']); + } + else + { + if ($this->debug) + { + print "
---PARSED---\n" ;
+					var_export($GLOBALS['_xh']['value']);
+					print "\n---END---
"; + } // note that using =& will raise an error if $GLOBALS['_xh']['st'] does not generate an object. + + $v =& $GLOBALS['_xh']['value']; + + if($GLOBALS['_xh']['isf']) + { + if ($return_type == 'xmlrpcvals') + { + $errno_v = $v->structmem('faultCode'); + $errstr_v = $v->structmem('faultString'); + $errno = $errno_v->scalarval(); + $errstr = $errstr_v->scalarval(); + } + else + { + $errno = $v['faultCode']; + $errstr = $v['faultString']; + } + + if($errno == 0) + { + // FAULT returned, errno needs to reflect that + $errno = -1; + } + + $r =& new xmlrpcresp($v, $errno, $errstr); + } + else + { + $r=&new xmlrpcresp($v, 0, '', 'phpvals'); + } + } + + $r->hdrs = $GLOBALS['_xh']['headers']; + $r->_cookies = $GLOBALS['_xh']['cookies']; + return $r; + } + } + + class xmlrpcval + { + var $me=array(); + var $mytype=0; + var $_php_class=null; + + function xmlrpcval($val=-1, $type='') + { + //$this->me=array(); + //$this->mytype=0; + if($val!==-1 || $type!='') + { + if($type=='') + { + $type='string'; + } + if($GLOBALS['xmlrpcTypes'][$type]==1) + { + $this->addScalar($val,$type); + } + elseif($GLOBALS['xmlrpcTypes'][$type]==2) + { + $this->addArray($val); + } + elseif($GLOBALS['xmlrpcTypes'][$type]==3) + { + $this->addStruct($val); + } + } + } + + function addScalar($val, $type='string') + { + $typeof=@$GLOBALS['xmlrpcTypes'][$type]; + if($typeof!=1) + { + error_log("XML-RPC: xmlrpcval::addScalar: not a scalar type ($typeof)"); + return 0; + } + + // coerce booleans into correct values + // NB: shall we do it for datetimes, integers and doubles, too? + if($type==$GLOBALS['xmlrpcBoolean']) + { + if(strcasecmp($val,'true')==0 || $val==1 || ($val==true && strcasecmp($val,'false'))) + { + $val=true; + } + else + { + $val=false; + } + } + + switch($this->mytype) + { + case 1: + error_log('XML-RPC: xmlrpcval::addScalar: scalar xmlrpcval can have only one value'); + return 0; + case 3: + error_log('XML-RPC: xmlrpcval::addScalar: cannot add anonymous scalar to struct xmlrpcval'); + return 0; + case 2: + // we're adding a scalar value to an array here + //$ar=$this->me['array']; + //$ar[]=&new xmlrpcval($val, $type); + //$this->me['array']=$ar; + // Faster (?) avoid all the costly array-copy-by-val done here... + $this->me['array'][]=&new xmlrpcval($val, $type); + return 1; + default: + // a scalar, so set the value and remember we're scalar + $this->me[$type]=$val; + $this->mytype=$typeof; + return 1; + } + } + + /// @todo add some checking for $vals to be an array of xmlrpcvals? + function addArray($vals) + { + if($this->mytype==0) + { + $this->mytype=$GLOBALS['xmlrpcTypes']['array']; + $this->me['array']=$vals; + return 1; + } + elseif($this->mytype==2) + { + // we're adding to an array here + $this->me['array'] = array_merge($this->me['array'], $vals); + } + else + { + error_log('XML-RPC: xmlrpcval::addArray: already initialized as a [' . $this->kindOf() . ']'); + return 0; + } + } + + /// @todo add some checking for $vals to be an array? + function addStruct($vals) + { + if($this->mytype==0) + { + $this->mytype=$GLOBALS['xmlrpcTypes']['struct']; + $this->me['struct']=$vals; + return 1; + } + elseif($this->mytype==3) + { + // we're adding to a struct here + $this->me['struct'] = array_merge($this->me['struct'], $vals); + } + else + { + error_log('XML-RPC: xmlrpcval::addStruct: already initialized as a [' . $this->kindOf() . ']'); + return 0; + } + } + + // poor man's version of print_r ??? + // DEPRECATED! + function dump($ar) + { + foreach($ar as $key => $val) + { + echo "$key => $val
"; + if($key == 'array') + { + while(list($key2, $val2) = each($val)) + { + echo "-- $key2 => $val2
"; + } + } + } + } + + function kindOf() + { + switch($this->mytype) + { + case 3: + return 'struct'; + break; + case 2: + return 'array'; + break; + case 1: + return 'scalar'; + break; + default: + return 'undef'; + } + } + + function serializedata($typ, $val, $charset_encoding='') + { + $rs=''; + switch(@$GLOBALS['xmlrpcTypes'][$typ]) + { + case 3: + // struct + if ($this->_php_class) + { + $rs.='\n"; + } + else + { + $rs.="\n"; + } + foreach($val as $key2 => $val2) + { + $rs.="${key2}\n"; + //$rs.=$this->serializeval($val2); + $rs.=$val2->serialize(); + $rs.="\n"; + } + $rs.=''; + break; + case 2: + // array + $rs.="\n\n"; + for($i=0; $iserializeval($val[$i]); + $rs.=$val[$i]->serialize(); + } + $rs.="\n"; + break; + case 1: + switch($typ) + { + case $GLOBALS['xmlrpcBase64']: + $rs.="<${typ}>" . base64_encode($val) . ""; + break; + case $GLOBALS['xmlrpcBoolean']: + $rs.="<${typ}>" . ($val ? '1' : '0') . ""; + break; + case $GLOBALS['xmlrpcString']: + // G. Giunta 2005/2/13: do NOT use htmlentities, since + // it will produce named html entities, which are invalid xml + $rs.="<${typ}>" . xmlrpc_encode_entitites($val, $GLOBALS['xmlrpc_internalencoding'], $charset_encoding). ""; + // $rs.="<${typ}>" . htmlentities($val). ""; + break; + case $GLOBALS['xmlrpcInt']: + case $GLOBALS['xmlrpcI4']: + $rs.="<${typ}>".(int)$val.""; + break; + case $GLOBALS['xmlrpcDouble']: + $rs.="<${typ}>".(double)$val.""; + break; + default: + // no standard type value should arrive here, but provide a possibility + // for xmlrpcvals of unknown type... + $rs.="<${typ}>${val}"; + } + break; + default: + break; + } + return $rs; + } + + /** + * Return xml representation of the value + * @param string $charset_encoding the charset to be used for serialization. if null, US-ASCII is assumed + */ + function serialize($charset_encoding='') + { + // add check? slower, but helps to avoid recursion in serializing broken xmlrpcvals... + //if (is_object($o) && (get_class($o) == 'xmlrpcval' || is_subclass_of($o, 'xmlrpcval'))) + //{ + reset($this->me); + list($typ, $val) = each($this->me); + return '' . $this->serializedata($typ, $val, $charset_encoding) . "\n"; + //} + } + + // DEPRECATED + function serializeval($o) + { + // add check? slower, but helps to avoid recursion in serializing broken xmlrpcvals... + //if (is_object($o) && (get_class($o) == 'xmlrpcval' || is_subclass_of($o, 'xmlrpcval'))) + //{ + $ar=$o->me; + reset($ar); + list($typ, $val) = each($ar); + return '' . $this->serializedata($typ, $val) . "\n"; + //} + } + + /** + * Checks wheter a struct member with a given name is present. + * Works only on xmlrpcvals of type struct. + * @param string $m the name of the struct member to be looked up + * @return boolean + */ + function structmemexists($m) + { + return array_key_exists($this->me['struct'][$m]); + } + + /* + * Returns the value of a given struct member (an xmlrpcval object in itself). + * Will raise a php warning if struct member of given name does not exist + * @param string $m the name of the struct member to be looked up + * @return xmlrpcval + */ + function structmem($m) + { + return $this->me['struct'][$m]; + } + + function structreset() + { + reset($this->me['struct']); + } + + function structeach() + { + return each($this->me['struct']); + } + + // DEPRECATED! this code looks like it is very fragile and has not been fixed + // for a long long time. Shall we remove it for 2.0? + function getval() + { + // UNSTABLE + reset($this->me); + list($a,$b)=each($this->me); + // contributed by I Sofer, 2001-03-24 + // add support for nested arrays to scalarval + // i've created a new method here, so as to + // preserve back compatibility + + if(is_array($b)) + { + @reset($b); + while(list($id,$cont) = @each($b)) + { + $b[$id] = $cont->scalarval(); + } + } + + // add support for structures directly encoding php objects + if(is_object($b)) + { + $t = get_object_vars($b); + @reset($t); + while(list($id,$cont) = @each($t)) + { + $t[$id] = $cont->scalarval(); + } + @reset($t); + while(list($id,$cont) = @each($t)) + { + //@eval('$b->'.$id.' = $cont;'); + @$b->$id = $cont; + } + } + // end contrib + return $b; + } + + /** + * Returns the value of a scalar xmlrpcval + * @return mixed + */ + function scalarval() + { + reset($this->me); + list(,$b)=each($this->me); + return $b; + } + + /** + * Returns the type of the xmlrpcval. + * For integers, 'int' is always returned in place of 'i4' + * @return string + */ + function scalartyp() + { + reset($this->me); + list($a,$b)=each($this->me); + if($a==$GLOBALS['xmlrpcI4']) + { + $a=$GLOBALS['xmlrpcInt']; + } + return $a; + } + + /** + * Returns the m-th member of an xmlrpcval of struct type + * @param integer $m the index of the value to be retrieved (zero based) + * @return xmlrpcval + */ + function arraymem($m) + { + return $this->me['array'][$m]; + } + + /** + * Returns the number of members in an xmlrpcval of array type + * @return integer + */ + function arraysize() + { + return count($this->me['array']); + } + + /** + * Returns the number of members in an xmlrpcval of struct type + * @return integer + */ + function structsize() + { + return count($this->me['struct']); + } + } + + + // date helpers + function iso8601_encode($timet, $utc=0) + { + // return an ISO8601 encoded string + // really, timezones ought to be supported + // but the XML-RPC spec says: + // + // "Don't assume a timezone. It should be specified by the server in its + // documentation what assumptions it makes about timezones." + // + // these routines always assume localtime unless + // $utc is set to 1, in which case UTC is assumed + // and an adjustment for locale is made when encoding + if(!$utc) + { + $t=strftime("%Y%m%dT%H:%M:%S", $timet); + } + else + { + if(function_exists('gmstrftime')) + { + // gmstrftime doesn't exist in some versions + // of PHP + $t=gmstrftime("%Y%m%dT%H:%M:%S", $timet); + } + else + { + $t=strftime("%Y%m%dT%H:%M:%S", $timet-date('Z')); + } + } + return $t; + } + + function iso8601_decode($idate, $utc=0) + { + // return a timet in the localtime, or UTC + $t=0; + if(ereg("([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})", $idate, $regs)) + { + if($utc) + { + $t=gmmktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]); + } + else + { + $t=mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]); + } + } + return $t; + } + + /** + * Takes an xmlrpc value in PHP xmlrpcval object format + * and translates it into native PHP types. + * Works with xmlrpc message objects as input, too. + * + * @author Dan Libby (dan@libby.com) + * + * @param xmlrpcval $xmlrpc_val + * @param array $options if 'decode_php_objs' is set in the options array, xmlrpc structs can be decoded into php objects + * @return mixed + */ + function php_xmlrpc_decode($xmlrpc_val, $options=array()) + { + switch($xmlrpc_val->kindOf()) + { + case 'scalar': + return $xmlrpc_val->scalarval(); + case 'array': + $size = $xmlrpc_val->arraysize(); + $arr = array(); + for($i = 0; $i < $size; $i++) + { + $arr[] = php_xmlrpc_decode($xmlrpc_val->arraymem($i), $options); + } + return $arr; + case 'struct': + $xmlrpc_val->structreset(); + // If user said so, try to rebuild php objects for specific struct vals. + /// @todo should we raise a warning for class not found? + // shall we check for proper subclass of xmlrpcval instead of + // presence of _php_class to detect what we can do? + if (in_array('decode_php_objs', $options) && $xmlrpc_val->_php_class != '' + && class_exists($xmlrpc_val->_php_class)) + { + $obj = @new $xmlrpc_val->_php_class; + while(list($key,$value)=$xmlrpc_val->structeach()) + { + $obj->$key = php_xmlrpc_decode($value, $options); + } + return $obj; + } + else + { + $arr = array(); + while(list($key,$value)=$xmlrpc_val->structeach()) + { + $arr[$key] = php_xmlrpc_decode($value, $options); + } + return $arr; + } + case 'msg': + $paramcount = $xmlrpc_val->getNumParams(); + $arr = array(); + for($i = 0; $i < $paramcount; $i++) + { + $arr[] = php_xmlrpc_decode($xmlrpc_val->getParam($i)); + } + return $arr; + } + } + + if(function_exists('xmlrpc_decode')) + { + define('XMLRPC_EPI_ENABLED','1'); + } + else + { + define('XMLRPC_EPI_ENABLED','0'); + } + + /** + * Takes native php types and encodes them into xmlrpc PHP object format. + * It will not re-encode xmlrpcval objects. + * Feature creep -- could support more types via optional type argument + * (string => datetime support has been added, ??? => base64 not yet) + * + * @author Dan Libby (dan@libby.com) + * + * @param mixed $php_val the value to be converted into an xmlrpcval object + * @param array $options can include 'encode_php_objs' and 'auto_dates' + * @return xmlrpcval + */ + function &php_xmlrpc_encode($php_val, $options=array()) + { + $type = gettype($php_val); + $xmlrpc_val =& new xmlrpcval; + + switch($type) + { + case 'array': + // PHP arrays can be encoded to either xmlrpc structs or arrays, + // depending on wheter they are hashes or plain 0..n integer indexed + // A shorter one-liner would be + // $tmp = array_diff(array_keys($php_val), range(0, count($php_val)-1)); + // but execution time skyrockets! + $j = 0; + $arr = array(); + $ko = false; + foreach($php_val as $key => $val) + { + $arr[$key] =& php_xmlrpc_encode($val, $options); + if(!$ko && $key !== $j) + { + $ko = true; + } + $j++; + } + if($ko) + { + $xmlrpc_val->addStruct($arr); + } + else + { + $xmlrpc_val->addArray($arr); + } + break; + case 'object': + if(is_a($php_val, 'xmlrpcval')) + { + $xmlrpc_val = $php_val; + } + else + { + $arr = array(); + while(list($k,$v) = each($php_val)) + { + $arr[$k] = php_xmlrpc_encode($v, $options); + } + $xmlrpc_val->addStruct($arr); + if (in_array('encode_php_objs', $options)) + { + // let's save original class name into xmlrpcval: + // might be useful later on... + $xmlrpc_val->_php_class = get_class($php_val); + } + } + break; + case 'integer': + $xmlrpc_val->addScalar($php_val, $GLOBALS['xmlrpcInt']); + break; + case 'double': + $xmlrpc_val->addScalar($php_val, $GLOBALS['xmlrpcDouble']); + break; + case 'string': + if (in_array('auto_dates', $options) && ereg("^[0-9]{8}\T{1}[0-9]{2}\:[0-9]{2}\:[0-9]{2}$", $php_val)) + $xmlrpc_val->addScalar($php_val, $GLOBALS['xmlrpcDateTime']); + else + $xmlrpc_val->addScalar($php_val, $GLOBALS['xmlrpcString']); + break; + // + // Add support for encoding/decoding of booleans, since they are supported in PHP + case 'boolean': + $xmlrpc_val->addScalar($php_val, $GLOBALS['xmlrpcBoolean']); + break; + // + // catch "resource", "NULL", "user function", "unknown type" + //case 'unknown type': + default: + // giancarlo pinerolo + // it has to return + // an empty object in case (which is already + // at this point), not a boolean. + break; + } + return $xmlrpc_val; + } + + /** + * decode a string that is encoded w/ "chunked" transfer encoding + * as defined in rfc2068 par. 19.4.6 + * code shamelessly stolen from nusoap library by Dietrich Ayala + * + * @param string $buffer the string to be decoded + * @return string + */ + function decode_chunked($buffer) + { + // length := 0 + $length = 0; + $new = ''; + + // read chunk-size, chunk-extension (if any) and crlf + // get the position of the linebreak + $chunkend = strpos($buffer,"\r\n") + 2; + $temp = substr($buffer,0,$chunkend); + $chunk_size = hexdec( trim($temp) ); + $chunkstart = $chunkend; + // while(chunk-size > 0) { + while($chunk_size > 0) + { + $chunkend = strpos($buffer, "\r\n", $chunkstart + $chunk_size); + + // just in case we got a broken connection + if($chunkend == false) + { + $chunk = substr($buffer,$chunkstart); + // append chunk-data to entity-body + $new .= $chunk; + $length += strlen($chunk); + break; + } + + // read chunk-data and crlf + $chunk = substr($buffer,$chunkstart,$chunkend-$chunkstart); + // append chunk-data to entity-body + $new .= $chunk; + // length := length + chunk-size + $length += strlen($chunk); + // read chunk-size and crlf + $chunkstart = $chunkend + 2; + + $chunkend = strpos($buffer,"\r\n",$chunkstart)+2; + if($chunkend == false) + { + break; //just in case we got a broken connection + } + $temp = substr($buffer,$chunkstart,$chunkend-$chunkstart); + $chunk_size = hexdec( trim($temp) ); + $chunkstart = $chunkend; + } + return $new; + } + + /** + * Given a string defining a php type or phpxmlrpc type (loosely defined: strings + * accepted come from javadoc blocks), return corresponding phpxmlrpc type. + * NB: for php 'resource' types returns empty string, since resources cannot be serialized; + * for php class names returns 'struct', since php objects can be serialized as xmlrpc structs + * @param string $phptype + * @return string + */ + function php_2_xmlrpc_type($phptype) + { + switch(strtolower($phptype)) + { + case 'string': + return $GLOBALS['xmlrpcString']; + case 'integer': + case $GLOBALS['xmlrpcInt']: // 'int' + case $GLOBALS['xmlrpcI4']: + return $GLOBALS['xmlrpcInt']; + case 'double': + return $GLOBALS['xmlrpcDouble']; + case 'boolean': + return $GLOBALS['xmlrpcBoolean']; + case 'array': + return $GLOBALS['xmlrpcArray']; + case 'object': + return $GLOBALS['xmlrpcStruct']; + case $GLOBALS['xmlrpcBase64']: + case $GLOBALS['xmlrpcStruct']: + return strtolower($phptype); + case 'resource': + return ''; + default: + if(class_exists($phptype)) + { + return $GLOBALS['xmlrpcStruct']; + } + else + { + // unknown: might be any xmlrpc type + return $GLOBALS['xmlrpcValue']; + } + } + } + + /** + * Given a user-defined PHP function, create a PHP 'wrapper' function that can + * be exposed as xmlrpc method from an xmlrpc_server object and called from remote + * clients. + * + * Since php is a typeless language, to infer types of input and output parameters, + * it relies on parsing the javadoc-style comment block associated with the given + * function. Usage of xmlrpc native types (such as datetime.dateTime.iso8601 and base64) + * in the @param tag is also allowed, if you need the php function to receive/send + * data in that particular format (note that base64 enncoding/decoding is transparently + * carried out by the lib, while datetime vals are passed around as strings) + * + * Known limitations: + * - requires PHP 5.0.3 + + * - only works for user-defined functions, not for PHP internal functions + * (reflection does not support retrieving number/type of params for those) + * - functions returning php objects will generate special xmlrpc responses: + * when the xmlrpc decoding of those responses is carried out by this same lib, using + * the appropriate param in php_xmlrpc_decode, the php objects will be rebuilt. + * In short: php objects can be serialized, too (except for their resource members), + * using this function. + * Other libs might choke on the very same xml that will be generated in this case + * (i.e. it has a nonstandard attribute on struct element tags) + * - usage of javadoc @param tags using param names in a different order from the + * function prototype is not considered valid (to be fixed?) + * + * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' + * php functions (ie. functions not expecting a single xmlrpcmsg obj as parameter) + * is by making use of the functions_parameters_type class member. + * + * @param string $funcname the name of the PHP user function to be exposed as xmlrpc method; array($obj, 'methodname') might be ok too, in the future... + * @return false on error, or an array containing the name of the new php function, + * its signature and docs, to be used in the server dispatch map + * + * @todo decide how to deal with params passed by ref: bomb out or allow? + * @todo finish using javadoc info to build method sig if all params are named but out of order + * @done switch to some automagic object encoding scheme + * @todo add a check for params of 'resource' type + * @todo add some trigger_errors when returning false? + * @todo what to do when the PHP function returns NULL? we are currently returning bogus responses!!! + */ + function wrap_php_function($funcname, $newfuncname='') + { + if(version_compare(phpversion(), '5.0.3') == -1) + { + // up to php 5.0.3 some useful reflection methods were missing + return false; + } + if((is_array($funcname) && !method_exists($funcname[0], $funcname[1])) || !function_exists($funcname)) + { + return false; + } + else + { + // determine name of new php function + if($newfuncname == '') + { + if(is_array($funcname)) + { + $xmlrpcfuncname = "xmlrpc_".implode('_', $funcname); + } + else + { + $xmlrpcfuncname = "xmlrpc_$funcname"; + } + } + else + { + $xmlrpcfuncname = $newfuncname; + } + while(function_exists($xmlrpcfuncname)) + { + $xmlrpcfuncname .= 'x'; + } + $code = "function $xmlrpcfuncname(\$msg) {\n"; + + // start to introspect PHP code + $func =& new ReflectionFunction($funcname); + if($func->isInternal()) + { + // Note: from PHP 5.1.0 onward, we will possibly be able to use invokeargs + // instead of getparameters to fully reflect internal php functions ? + return false; + } + + // retrieve parameter names, types and description from javadoc comments + + // function description + $desc = ''; + // type of return val: by default 'any' + $returns = $GLOBALS['xmlrpcValue']; + // type + name of function parameters + $paramDocs = array(); + + $docs = $func->getDocComment(); + if($docs != '') + { + $docs = explode("\n", $docs); + $i = 0; + foreach($docs as $doc) + { + $doc = trim($doc, " \r\t/*"); + if(strlen($doc) && strpos($doc, '@') !== 0 && !$i) + { + if($desc) + { + $desc .= "\n"; + } + $desc .= $doc; + } + elseif(strpos($doc, '@param') === 0) + { + // syntax: @param type [$name] desc + if(preg_match('/@param\s+(\S+)(\s+\$\S+)?\s+(.+)/', $doc, $matches)) + { + if(strpos($matches[1], '|')) + { + //$paramDocs[$i]['type'] = explode('|', $matches[1]); + $paramDocs[$i]['type'] = 'mixed'; + } + else + { + $paramDocs[$i]['type'] = $matches[1]; + } + $paramDocs[$i]['name'] = trim($matches[2]); + $paramDocs[$i]['doc'] = $matches[3]; + } + $i++; + } + elseif(strpos($doc, '@return') === 0) + { + $returns = preg_split("/\s+/", $doc); + if(isset($returns[1])) + { + $returns = php_2_xmlrpc_type($returns[1]); + } + } + } + } + + // start introspection of actual function prototype and building of PHP code + // to be eval'd + $params = $func->getParameters(); + + $innercode = ''; + $i = 0; + $parsvariations = array(); + $pars = array(); + $pnum = count($params); + foreach($params as $param) + { + if (isset($paramDocs[$i]['name']) && $paramDocs[$i]['name'] && strtolower($paramDocs[$i]['name']) != '$'.strtolower($param->getName())) + { + // param name from phpdoc info does not match param definition! + $paramDocs[$i]['type'] = 'mixed'; + } + + if($param->isOptional()) + { + // this particular parameter is optional. save as valid previous list of parameters + $innercode .= "if (\$paramcount > $i) {\n"; + $parsvariations[] = $pars; + } + $innercode .= "\$p$i = \$msg->getParam($i);\n"; + $innercode .= "if (\$p{$i}->kindOf() == 'scalar') \$p$i = \$p{$i}->scalarval(); else \$p$i = php_xmlrpc_decode(\$p$i);\n"; + $pars[] = "\$p$i"; + $i++; + if($param->isOptional()) + { + $innercode .= "}\n"; + } + if($i == $pnum) + { + // last allowed parameters combination + $parsvariations[] = $pars; + } + } + + $sigs = array(); + if(count($parsvariations) == 0) + { + // only known good synopsis = no parameters + $parsvariations[] = array(); + $minpars = 0; + } + else + { + $minpars = count($parsvariations[0]); + } + + if($minpars) + { + // add to code the check for min params number + // NB: this check needs to be done BEFORE decoding param values + $innercode = "\$paramcount = \$msg->getNumParams();\n" . + "if (\$paramcount < $minpars) return new xmlrpcresp(0, {$GLOBALS['xmlrpcerr']['incorrect_params']}, '{$GLOBALS['xmlrpcstr']['incorrect_params']}');\n" . $innercode; + } + else + { + $innercode = "\$paramcount = \$msg->getNumParams();\n" . $innercode; + } + + $innercode .= "\$np = false;"; + foreach($parsvariations as $pars) + { + $innercode .= "if (\$paramcount == " . count($pars) . ") \$retval = $funcname(" . implode(',', $pars) . "); else\n"; + // build a 'generic' signature (only use an appropriate return type) + $sig = array($returns); + for($i=0; $i < count($pars); $i++) + { + if (isset($paramDocs[$i]['type'])) + { + $sig[] = php_2_xmlrpc_type($paramDocs[$i]['type']); + } + else + { + $sig[] = $GLOBALS['xmlrpcValue']; + } + } + $sigs[] = $sig; + } + $innercode .= "\$np = true;\n"; + $innercode .= "if (\$np) return new xmlrpcresp(0, {$GLOBALS['xmlrpcerr']['incorrect_params']}, '{$GLOBALS['xmlrpcstr']['incorrect_params']}'); else\n"; + //$innercode .= "if (\$_xmlrpcs_error_occurred) return new xmlrpcresp(0, $GLOBALS['xmlrpcerr']user, \$_xmlrpcs_error_occurred); else\n"; + if($returns == $GLOBALS['xmlrpcDateTime'] || $returns == $GLOBALS['xmlrpcBase64']) + { + $innercode .= "return new xmlrpcresp(new xmlrpcval(\$retval, '$returns'));"; + } + else + { + $innercode .= "return new xmlrpcresp(php_xmlrpc_encode(\$retval, array('encode_php_objs')));"; + } + // shall we exclude functions returning by ref? + // if($func->returnsReference()) + // return false; + $code = $code . $innercode . "\n}\n \$allOK=1;"; + //print_r($code); + $allOK = 0; + eval($code); + // alternative + //$xmlrpcfuncname = create_function('$m', $innercode); + + if(!$allOK) + { + return false; + } + + /// @todo examine if $paramDocs matches $parsvariations and build array for + /// usage as method signature, plus put together a nice string for docs + + $ret = array('function' => $xmlrpcfuncname, 'signature' => $sigs, 'docstring' => $desc); + return $ret; + } + } + + /** + * Given an xmlrpc client and a method name, register a php wrapper function + * that will call it and return results using native php types for both + * params and results. The generated php function will return an xmlrpcresp + * oject for failed xmlrpc calls + * + * Known limitations: + * - server must support system.methodsignature for the wanted xmlrpc method + * - for methods that expose many signatures, only one can be picked (we + * could in priciple check if signatures differ only by number of params + * and not by type, but it would be more complication than we can spare time) + * - nested xmlrpc params: the caller of the generated php function has to + * encode on its own the params passed to the php function if these are structs + * or arrays whose (sub)members include values of type datetime or base64 + * + * Notes: the connection properties of the given client will be copied + * and reused for the connection used during the call to the generated + * php function. + * Calling the generated php function 'might' be slow: a new xmlrpc client + * is created on every invocation and an xmlrpc-connection opened+closed. + * An extra 'debug' param is appended to param list of xmlrpc method, useful + * for debugging purposes. + * + * @param xmlrpc_client $client an xmlrpc client set up correctly to communicate with target server + * @param string $methodname the xmlrpc method to be mapped to a php function + * @param integer $signum the index of the method signature to use in mapping (if method exposes many sigs) + * @return string the name of the generated php function (or false) + */ + function wrap_xmlrpc_method($client, $methodname, $signum=0, $timeout=0, $protocol='', $newfuncname='') + { + $msg =& new xmlrpcmsg('system.methodSignature'); + $msg->addparam(new xmlrpcval($methodname)); + $response =& $client->send($msg, $timeout, $protocol); + if(!$response || $response->faultCode()) + { + return false; + } + else + { + $desc = $response->value(); + if($desc->kindOf() != 'array' || $desc->arraysize() <= $signum) + { + return false; + } + else + { + if($newfuncname != '') + { + $xmlrpcfuncname = $newfuncname; + } + else + { + $xmlrpcfuncname = 'xmlrpc_'.str_replace('.', '_', $methodname); + } + while(function_exists($xmlrpcfuncname)) + { + $xmlrpcfuncname .= 'x'; + } + $desc = $desc->arraymem($signum); + $code = "function $xmlrpcfuncname ("; + $innercode = "\$client =& new xmlrpc_client('$client->path', '$client->server');\n"; + // copy all client fields to the client that will be generated runtime + // (this provides for future expansion of client obj) + foreach($client as $fld => $val) + { + if($fld != 'debug' && $fld != 'return_type') + { + $val = var_export($val, true); + $innercode .= "\$client->$fld = $val;\n"; + } + } + $innercode .= "\$client->setDebug(\$debug);\n"; + $innercode .= "\$client->return_type = 'xmlrpcvals';\n"; + $innercode .= "\$msg =& new xmlrpcmsg('$methodname');\n"; + + // param parsing + $plist = array(); + $pcount = $desc->arraysize(); + for($i = 1; $i < $pcount; $i++) + { + $plist[] = "\$p$i"; + $ptype = $desc->arraymem($i); + $ptype = $ptype->scalarval(); + if($ptype == 'dateTime.iso8601' || $ptype == 'base64') + { + $innercode .= "\$p$i =& new xmlrpcval(\$p$i, '$ptype');\n"; + } + else + { + $innercode .= "\$p$i =& php_xmlrpc_encode(\$p$i);\n"; + } + $innercode .= "\$msg->addparam(\$p$i);\n"; + } + $plist[] = '$debug = 0'; + $plist = implode(',', $plist); + + $innercode .= "\$res =& \$client->send(\$msg, $timeout, '$protocol');\n"; + $innercode .= "if (\$res->faultcode()) return \$res; else return php_xmlrpc_decode(\$res->value(), array('decode_php_objs'));"; + + $code = $code . $plist. ") {\n" . $innercode . "\n}\n\$allOK=1;"; + //print_r($code); + $allOK = 0; + eval($code); + // alternative + //$xmlrpcfuncname = create_function('$m', $innercode); + if($allOK) + { + return $xmlrpcfuncname; + } + else + { + return false; + } + } + } + } + + /** + * xml charset encoding guessing helper function. + * Tries to determine the charset encoding of an XML chunk + * received over HTTP. + + * NB: according to the spec (RFC 3023, if text/xml content-type is received over HTTP without a content-type, + + * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of unconforming (legacy?) clients/servers, + + * which will be most probably using UTF-8 anyway... + * + * @param string $httpheaders the http Content-type header + * @param string $xmlchunk xml content buffer + * @param string $encoding_prefs comma separated list of character encodings to be used as default (when mb extension is enabled) + * + * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!! + */ + function guess_encoding($httpheader='', $xmlchunk='', $encoding_prefs=null) + { + // discussion: see http://www.yale.edu/pclt/encoding/ + // 1 - test if encoding is specified in HTTP HEADERS + + //Details: + // LWS: (\13\10)?( |\t)+ + // token: (any char but excluded stuff)+ + // header: Content-type = ...; charset=value(; ...)* + // where value is of type token, no LWS allowed between 'charset' and value + // Note: we do not check for invalid chars in VALUE: + // this had better be done using pure ereg as below + + /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it? + if(eregi(";((\\xD\\xA)?[ \\x9]+)*charset=", $httpheader)) + { + /// @BUG if charset is received uppercase, this line will fail! + $in = strpos($httpheader, 'charset=')+8; + $out = strpos($httpheader, ';', $in) ? strpos($httpheader, ';', $in) : strlen($httpheader); + return strtoupper(trim(substr($httpheader, $in, $out-$in))); + } + + // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern + // (source: http://www.w3.org/TR/2000/REC-xml-20001006) + // NOTE: actually, according to the spec, even if we find the BOM and determine + // an encoding, we should check if there is an encoding specified + // in the xml declaration, and verify if they match. + /// @todo implement check as described above? + /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM) + if(@ereg("^(\\x00\\x00\\xFE\\xFF|\\xFF\\xFE\\x00\\x00|\\x00\\x00\\xFF\\xFE|\\xFE\\xFF\\x00\\x00)", $xmlchunk)) + // if (preg_match("/^(\\x00\\x00\\xFE\\xFF|\\xFF\\xFE\\x00\\x00|\\x00\\x00\\xFF\\xFE|\\xFE\\xFF\\x00\\x00)/", $xmlchunk)) + { + return 'UCS-4'; + } + elseif(ereg("^(\\xFE\\xFF|\\xFF\\xFE)", $xmlchunk)) + { + return 'UTF-16'; + } + elseif(ereg("^(\\xEF\\xBB\\xBF)", $xmlchunk)) + { + return 'UTF-8'; + } + + // 3 - test if encoding is specified in the xml declaration + // Details: + // SPACE: (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+ + // EQ: SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]* + if (ereg("^<\?xml". + "[ \\x9\\xD\\xA]+" . "version" . "[ \\x9\\xD\\xA]*=[ \\x9\\xD\\xA]*" . "((\"[a-zA-Z0-9_.:-]+\")|('[a-zA-Z0-9_.:-]+'))". + "[ \\x9\\xD\\xA]+" . "encoding" . "[ \\x9\\xD\\xA]*=[ \\x9\\xD\\xA]*" . "((\"[A-Za-z][A-Za-z0-9._-]*\")|('[A-Za-z][A-Za-z0-9._-]*'))", + $xmlchunk, $regs)) + { + return strtoupper(substr($regs[4], 1, strlen($regs[4])-2)); + } + + // 4 - if mbstring is available, let it do the guesswork + // NB: we favour finding an encoding that is compatible with what we can process + if(extension_loaded('mbstring')) + { + if($encoding_prefs) + { + $enc = mb_detect_encoding($xmlchunk, $encoding_prefs); + } + else + { + $enc = mb_detect_encoding($xmlchunk); + } + // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII... + // IANA also likes better US-ASCII, so go with it + if($enc == 'ASCII') + { + $enc = 'US-'.$enc; + } + return $enc; + } + else + { + // no encoding specified: as per HTTP1.1 assume it is iso-8859-1? + // Both RFC 2616 (HTTP 1.1) and 1945(http 1.0) clearly state that for text/xxx content types + // this should be the standard. And we should be getting text/xml as request and response. + // BUT we have to be backward compatible with the lib, which always used UTF-8 as default... + return $GLOBALS['xmlrpc_defencoding']; + } + } + +/** +* Checks if a given charset encoding is present in a list of encodings or +* if it is a valid subset of any encoding in the list +* @param string $encoding charset to be tested +* @param mixed $validlist comma separated list of valid charsets (or array of charsets) +*/ +function is_valid_charset($encoding, $validlist) +{ + $charset_supersets = array( + 'US-ASCII' => array ('ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', + 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', + 'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-11', 'ISO-8859-12', + 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'UTF-8', + 'EUC-JP', 'EUC-', 'EUC-KR', 'EUC-CN') + ); + if (is_string($validlist)) + $validlist = split(',', $validlist); + if (@in_array(strtoupper($encoding), $validlist)) + return true; + else + { + if (array_key_exists($encoding, $charset_supersets)) + foreach ($validlist as $allowed) + if (in_array($allowed, $charset_supersets[$encoding])) + return true; + return false; + } +} + +?> \ No newline at end of file diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv index b13bc586b9f..f78044ea38e 100644 --- a/addons/crm/security/ir.model.access.csv +++ b/addons/crm/security/ir.model.access.csv @@ -3,43 +3,30 @@ "access_crm_segmentation_line","crm.segmentation.line","model_crm_segmentation_line","crm.group_crm_manager",1,1,1,1 "access_crm_case_section","crm.case.section","model_crm_case_section","crm.group_crm_user",1,0,0,0 "access_crm_case_categ","crm.case.categ","model_crm_case_categ","crm.group_crm_user",1,0,0,0 -"access_crm_case_manger","crm.case manager","model_crm_case","crm.group_crm_user",1,1,1,1 -"access_crm_case","crm.case","model_crm_case","crm.group_crm_manager",1,1,1,1 -"access_crm_claim","crm.claim","model_crm_claim","crm.group_crm_manager",1,1,1,1 -"access_crm_fundraising","crm.fundraising","model_crm_fundraising","crm.group_crm_manager",1,1,1,1 -"access_crm_helpdesk","crm.helpdesk","model_crm_helpdesk","crm.group_crm_manager",1,1,1,1 "access_crm_meeting","crm.meeting","model_crm_meeting","crm.group_crm_manager",1,1,1,1 "access_crm_lead","crm.lead","model_crm_lead","crm.group_crm_manager",1,1,1,1 -"access_crm_opportunity","crm.opportunity","model_crm_opportunity","crm.group_crm_manager",1,1,1,1 "access_crm_phonecall","crm.phonecall","model_crm_phonecall","crm.group_crm_manager",1,1,1,1 "access_crm_case_log","crm.case.log","model_crm_case_log","crm.group_crm_user",1,1,1,1 "access_crm_case_history","crm.case.history","model_crm_case_history","crm.group_crm_user",1,1,1,1 -"access_crm_case_section_manager","crm.case.section.manager","model_crm_case_section","crm.group_crm_manager",1,1,1,1 +"o","crm.case.section.manager","model_crm_case_section","crm.group_crm_manager",1,1,1,1 "access_crm_case_categ_manager","crm.case.categ.manager","model_crm_case_categ","crm.group_crm_manager",1,1,1,1 "access_crm_case_log_manager","crm.case.log manager","model_crm_case_log","crm.group_crm_manager",1,1,1,1 "access_crm_case_history_manager","crm.case.history manager","model_crm_case_history","crm.group_crm_manager",1,1,1,1 -"access_crm_email_add_cc_manager","crm.email.add.cc","model_crm_email_add_cc","crm.group_crm_manager",1,1,1,1 -"access_crm_email_add_cc_user","crm.email.add.cc","model_crm_email_add_cc","crm.group_crm_user",1,0,0,0 "access_crm_case_stage","crm.case.stage","model_crm_case_stage","crm.group_crm_user",1,0,0,0 "access_crm_case_stage_manager","crm.case.stage","model_crm_case_stage","crm.group_crm_manager",1,1,1,1 "access_crm_case_resource_type_user","crm_case_resource_type user","model_crm_case_resource_type","crm.group_crm_user",1,0,0,0 "access_crm_case_resource_type_manager","crm_case_resource_type manager","model_crm_case_resource_type","crm.group_crm_manager",1,1,1,1 -"access_crm_case_report_user","crm.case.report","model_crm_case_report","crm.group_crm_user",1,0,0,0 -"access_crm_case_report_manager","crm.case.report manager","model_crm_case_report","crm.group_crm_manager",1,1,1,1 -"access_crm_claim_report_user","crm.claim.report","model_crm_claim_report","crm.group_crm_user",1,0,0,0 -"access_crm_fundraising_report_user","crm.fundraising.report","model_crm_fundraising_report","crm.group_crm_user",1,0,0,0 "access_crm_lead_report_user","crm.lead.report","model_crm_lead_report","crm.group_crm_user",1,0,0,0 "access_crm_phonecall_report_user","crm.phonecall.report","model_crm_phonecall_report","crm.group_crm_user",1,0,0,0 -"access_crm_opportunity_report_user","crm.opportunity.report","model_crm_opportunity_report","crm.group_crm_user",1,0,0,0 -"access_report_crm_case_service_dashboard","report.crm.case.service.dashboard","model_report_crm_case_service_dashboard","crm.group_crm_user",1,0,0,0 "access_crm_lead2partner","crm.lead2partner","model_crm_lead2partner","crm.group_crm_user",1,1,1,1 "access_crm_lead2opportunity","crm.lead2opportunity","model_crm_lead2opportunity","crm.group_crm_user",1,1,1,1 "access_crm_opportunity2phonecall","crm.opportunity2phonecall","model_crm_opportunity2phonecall","crm.group_crm_user",1,1,1,1 "access_crm_phonecall2phonecall","crm.phonecall2phonecall","model_crm_phonecall2phonecall","crm.group_crm_user",1,1,1,1 "access_crm_phonecall2partner","crm.phonecall2partner","model_crm_phonecall2partner","crm.group_crm_user",1,1,1,1 "access_crm_phonecall2opportunity","crm.phonecall2opportunity","model_crm_phonecall2opportunity","crm.group_crm_user",1,1,1,1 -"access_report_crm_helpdesk","report.crm.helpdesk","model_crm_helpdesk_report","crm.group_crm_user",1,1,1,1 "access_crm_send_mail","crm.send.mail","model_crm_send_mail","crm.group_crm_user",1,1,1,1 "access_crm_send_mail_attachment","crm.send.mail.attachment","model_crm_send_mail_attachment","crm.group_crm_user",1,1,1,1 "access_crm_partner2opportunity","crm.partner2opportunity","model_crm_partner2opportunity","crm.group_crm_user",1,1,1,1 "access_crm_lead2opportunity_partner","crm.lead2opportunity.partner","model_crm_lead2opportunity_partner","crm.group_crm_user",1,1,1,1 +"access_crm_installer","crm.installer.rule","model_crm_installer","crm.group_crm_user",1,1,1,1 +"access_crm_lead_forward_to_partner","crm.lead.forward.to.partner","model_crm_lead_forward_to_partner","crm.group_crm_user",2,2,2,2 diff --git a/addons/crm/test/test_crm_lead.yml b/addons/crm/test/test_crm_lead.yml new file mode 100644 index 00000000000..0a051d2310d --- /dev/null +++ b/addons/crm/test/test_crm_lead.yml @@ -0,0 +1,112 @@ +- | + In order to test the CRM in OpenERP, + I will do a customer qualification process that + starts with a fist contact with a customer (a lead), which will be converted to a + business opportunity and a partner. +- | + In order to test the flow, I create a two new users "user_crm" and + I assign the group "salesman". +- + !record {model: res.users, id: res_users_usercrm0}: + company_id: base.main_company + context_lang: en_US + context_section_id: crm.section_sales_department + groups_id: + - crm.group_crm_user + login: user_crm + name: user_crm + password: user_crm + + +- | + I start by creating a new lead "New Customer" and I provide an address to this + new customer, as well as an email "info@mycustomer.com". +- + !record {model: crm.lead, id: crm_lead_newcustomer0}: + email_from: info@mycustomer.com + name: New Customer + partner_name: Sandip zala + phone: (855) 924-4364 + mobile: (333) 715-1450 + section_id: crm.section_sales_department + referred: False + title: M. +- | + I check that the lead is in 'draft' state, +- + !assert {model: crm.lead, id: crm_lead_newcustomer0, string: Lead in Draft}: + - state == "draft" +- + I open lead by click on "Open" button, +- + !python {model: crm.lead}: | + self.case_open(cr, uid, [ref("crm_lead_newcustomer0")]) + +- | + As the lead seems to be a real business opportunity, I will convert it to a + partner + and a business opportunity by clicking on the "Convert" button. +- + !python {model: crm.lead}: | + lead = self.browse(cr, uid, ref('crm_lead_newcustomer0')) + if not lead.partner_id: + self.convert_opportunity(cr, uid, [ref("crm_lead_newcustomer0")], {'active_ids': [ref("crm_lead_newcustomer0")]}) +- + | + Now, select "create a new partner" option in this wizard. +- + !record {model: crm.lead2opportunity.partner, id: crm_lead2opportunity_partner_create_0}: + action: 'create' +- + I click on "Continue" button of this wizard. +- + !python {model: crm.lead2opportunity.partner}: | + self.make_partner(cr, uid, [ref("crm_lead2opportunity_partner_create_0")], {'active_ids': [ref("crm_lead_newcustomer0")]}) +- + Now, I give value to this wizard field. +- + !record {model: crm.lead2opportunity, id: crm_lead2opportunity_stonage_0}: + name: Sandip zala + planned_revenue: 0.00 + probability: 0.00 +- + Then, Click on "Create Opportunity" button of this wizard. +- + !python {model: crm.lead2opportunity}: | + self.action_apply(cr, uid, [ref('crm_lead2opportunity_stonage_0')], {'active_id': ref('crm_lead_newcustomer0')}) + +- | + I can check that a lead and a business opportunity is now assigned to this + lead. +- + # !python {model: crm.lead, id: crm_lead_newcustomer0}: + # - opportunity_id.id != False + +- | + I check that the partner associated to this lead as the same country, phone number + and name than the opportunity. +- + !python {model: crm.lead}: | + lead = self.browse(cr, uid, ref("crm_lead_newcustomer0")) + obj_opportunity = self.pool.get('crm.lead') + ids = obj_opportunity.search(cr, uid, [('name', '=', lead.partner_name)]) + opportunity = obj_opportunity.browse(cr, uid, ids)[0] + assert lead.partner_name == opportunity.partner_id.name + assert lead.phone == opportunity.phone +- + | + # yaml is also not working smpt server and send new email. + +- | + I configure with smtp server. +- | + And I communicate with lead through send New mail to Lead using it email address from user which are loged. +- | + I check that communication history generated when send email to lead. +- | + Then, I add a cc which receive copy of future communication between partner and users by mail. + +- | + I Reply to last Email to lead with some document attached.and check that communication history generated or not. + + \ No newline at end of file diff --git a/addons/crm/test/test_crm_meeting.yml b/addons/crm/test/test_crm_meeting.yml new file mode 100644 index 00000000000..6365c913b1f --- /dev/null +++ b/addons/crm/test/test_crm_meeting.yml @@ -0,0 +1,99 @@ +- | + Now I will test Meetings which may be customer meeting or phonecall meeting or + internal Meeting. +- | + I start by creating a new Meeting. +- + !record {model: crm.meeting, id: crm_meeting_regardingpresentation0}: + categ_id: crm.categ_meet2 + date: '2010-04-21 16:04:00' + date_deadline: '2010-04-22 00:04:00' + duration: 8.0 + email_from: info@balmerinc.be + location: Ahmedabad + name: Regarding Presentation + partner_address_id: base.res_partner_address_1 + partner_id: base.res_partner_9 + rrule_type: weekly + section_id: crm.section_sales_department +- | + I check that the Meetings is in 'UnConfirmed' state. +- + !assert {model: crm.meeting, id: crm_meeting_regardingpresentation0}: + - state == "draft" +- | + I can set reminder on meeting if I put reminder "40 minutes before" +- | + For that, I first create alarm. +- + !record {model: res.alarm, id: res_alarm_minituesbefore0}: + name: 40 minutes before + trigger_duration: 40 + trigger_interval: minutes + trigger_occurs: before + trigger_related: start +- | + Now I will assign this reminder. + +- !python {model: crm.meeting}: | + self.write(cr, uid, [ref('crm_meeting_regardingpresentation0')], {'alarm_id': ref("res_alarm_minituesbefore0")}) +- | + In order to check recurrence on meetings I will set Recurrency to Custom + and I set the fields so that the meeting will occure weekly on Monday and Friday 10 times +- + !python {model: crm.meeting}: | + self.write(cr, uid, [ref("crm_meeting_regardingpresentation0")], {'fr': 1, 'mo': 1, 'th': 1, 'tu': 1, 'we':1, 'count':10, 'interval': 1, 'freq': 'weekly', 'rrule_type': 'custom'}) + +- | + I can see from the calendar view that the meeting is scheduled on Monday and Friday + for 10 times, + +- + !python {model: crm.meeting}: | + self.fields_view_get(cr, uid, False, 'calendar', context) +- | + I will search for one of the recurrent event and count the number of meeting. +- + !python {model: crm.meeting}: | + ids = self.search(cr, uid, [('date', '>=', '2010-05-21 00:00:00'), ('date', '<=', '2010-04-21 00:00:00')] ) + assert len(ids) == 9 + +- | + Now If I want to edit meetings information for all occurence I click on "Edit All" button. +- + !python {model: crm.meeting}: | + self.open_meeting(cr, uid, [ref('crm_meeting_regardingpresentation0')]) +- | + I can see that new meeting form is opened with same value + I change some data for meeting and save it + I can see from meeting's calendar view that all meeting occurences are changed accordingly +- + !record {model: crm.meeting, id: crm.crm_meeting_regardingpresentation0}: + alarm_id: base_calendar.alarm9 + rrule_type: weekly + +- | + In order to invite people for this meetings, I click on "Invite People" button + I can invite internal user. +- + !record {model: base_calendar.invite.attendee, id: base_calendar_invite_attendee_0}: + type: internal + partner_id: base.res_partner_9 + user_ids: + - base.user_demo +- | + If I set Send mail boolean as True I can see that an email is send to + specified email address with proper meeting information. +- + #This is not working for send mail boolean as True. +- + I click on "Invite" button of "Invite attendee" wizard. +- + !python {model: base_calendar.invite.attendee}: | + self.do_invite(cr, uid, [ref('base_calendar_invite_attendee_0')], {'active_id': ref('crm_meeting_regardingpresentation0'), 'model' : 'crm.meeting', 'attendee_field':'attendee_ids'}) + +- | + After direct/indirect confirmation for meetings I can confirm meeting +- + !python {model: crm.meeting}: | + self.case_open(cr, uid, [ref('crm_meeting_regardingpresentation0')]) diff --git a/addons/crm/test/test_crm_opportunity.yml b/addons/crm/test/test_crm_opportunity.yml new file mode 100644 index 00000000000..e3a4f28cb5e --- /dev/null +++ b/addons/crm/test/test_crm_opportunity.yml @@ -0,0 +1,81 @@ +- | + I start by creating a new Opportunity. And I select partner for opportunity. + I can see that after selecting partner his contact and email is automatically filled. +- + !record {model: crm.lead, id: crm_opportunity_abcfuelcounits0}: + email_from: info@balmerinc.be + name: 'ABC FUEL CO 829264 - 10002 units ' + partner_address_id: base.res_partner_address_1 + partner_id: base.res_partner_9 + probability: 1.0 + stage_id: crm.stage_oppor1 + categ_id: crm.categ_oppor2 + section_id: crm.section_sales_department + +- | + I check that the opportunity is in 'New' state. +- + !assert {model: crm.lead, id: crm_opportunity_abcfuelcounits0}: + - state == "draft" +- | + I open opportunity by click on "Open" button, +- + !python {model: crm.lead}: | + self.case_open(cr, uid, [ref("crm_opportunity_abcfuelcounits0")]) + +- | + I schedule Meeting on this current opportunity by clicking on "schedule + Meeting". +- + !python {model: crm.lead}: | + self.action_makeMeeting(cr, uid, [ref("crm_opportunity_abcfuelcounits0")]) + +- | + I can see that Meeting's calendar view is shown. + then I click on the date on which I want schedule meeting. + I fill proper data for that meeting and save it +- + !record {model: crm.meeting, id: crm_meeting_abcfuelcounits0}: + alarm_id: base_calendar.alarm3 + date: '2010-04-16 00:00:00' + date_deadline: '2010-04-16 08:00:00' + duration: 8.0 + email_from: info@balmerinc.be + name: 'ABC FUEL CO 829264 - 10002 units ' + opportunity_id: 'crm_opportunity_abcfuelcounits0' + partner_address_id: base.res_partner_address_1 + partner_id: base.res_partner_9 + rrule_type: weekly + section_id: crm.section_sales_department + state: open +- | + In order to schedule a phonecall to the partner + I click on "schedule call" button and select planned date for the call. +- + !record {model: crm.opportunity2phonecall, id: crm_opportunity2phonecall_abcfuelcounits0}: + date: '2010-04-17 11:15:00' + name: 'ABC FUEL CO 829264 - 10002 units ' + section_id: crm.section_sales_department + user_id: base.user_demo +- | + schedule phonecall by apply (action_apply)function of opportunity2phoncall wizard +- + !python {model: crm.opportunity2phonecall}: | + self.action_apply(cr, uid, [ref('crm_opportunity2phonecall_abcfuelcounits0')], {'active_ids': [ref("crm_opportunity_abcfuelcounits0")]}) +- | + I check that phonecall record is created for that opportunity. +- + !python {model: crm.phonecall}: | + ids = self.search(cr, uid, [('name', '=', 'ABC FUEL CO 829264 - 10002 units')]) + assert len(ids) +- | + I can see phonecall record after click on "Schedule call" wizard. +- + !record {model: crm.phonecall, id: crm_phonecall_abcfuelcounits0}: + date: '2010-04-17 11:15:00' + duration: 3.0 + name: 'ABC FUEL CO 829264 - 10002 units' + partner_address_id: base.res_partner_address_1 + partner_id: base.res_partner_9 + section_id: crm.section_sales_department + \ No newline at end of file diff --git a/addons/crm/test/test_crm_phonecall.yml b/addons/crm/test/test_crm_phonecall.yml new file mode 100644 index 00000000000..feb87e0a879 --- /dev/null +++ b/addons/crm/test/test_crm_phonecall.yml @@ -0,0 +1,91 @@ +- | + I start by creating a new phonecall. +- + !record {model: crm.phonecall, id: crm_phonecall_interviewcall0}: + date: '2010-04-21 18:59:00' + name: Interview call + partner_address_id: base.res_partner_address_1 + partner_id: base.res_partner_9 + partner_mobile: (+32)2 211 34 83 + section_id: crm.section_sales_department + +- | + As the success of phonecall seems to be a real business opportunity, I will convert + it to opportunity by clicking on the "Convert to Opportunity" button. +- +# !python {model: crm.phonecall2opportunity}: | +# self.action_apply(cr, uid, [1], {'active_id': ref('crm_phonecall_interviewcall0')}) + +- + # This is not working, find a way to do that in YAML + +- | + I can see that a business opportunity is now assigned to this phonecall +- | + I check that the partner associated to this as the same country, phone number + and name than the phonecall. +- | + In order to avoid the duplication of similar partner, OpenERP should be able to + detect if the partner exists or if it should be created from the phonecall during the + conversion. +- | + In order to test this, I log as the user "user_crm2" +- | + Then, I create the same phonecall than the preceeding one but I change the name + of the phonecall. But this customer keeps the same email. +- | + This time OpenERP should detect that this customer already exists in the + partner base and I check that in the wizard, proposes me to link to this existing + partner instead of creating another one. +- | + I confirm the conversion wizard. +- | + And I check that the phonecall and the newly created business opportunity is linked + to the partner +- | + I schedule Meeting on this current phonecall by clicking on "schedule + Meeting" +- + !python {model: crm.phonecall}: | + self.action_make_meeting(cr, uid, [ref('crm_phonecall_interviewcall0')]) + +- | + I can see that Meeting's calendar view is shown. + then I click on the date on which I want schedule meeting. + I fill proper data for that meeting and save it +- + !record {model: crm.meeting, id: crm_meeting_interviewcall0}: + alarm_id: base_calendar.alarm3 + date: '2010-04-20 00:00:00' + date_deadline: '2010-04-20 08:00:00' + duration: 8.0 + email_from: info@balmerinc.be + name: Interview call + partner_address_id: base.res_partner_address_1 + partner_id: base.res_partner_9 + phonecall_id: 'crm_phonecall_interviewcall0' + rrule_type: weekly + state: open + +- + #This is not working for yaml +- | + I can jump to this meeting by "Meetings" shortcut from my phonecall's form view +- + +- | + In order to schedule other phonecall to the partner + I click on "schedule other call" button. and plan for other call + I can see that it will open other phonecall view with some data same as current + phonecall. +- + !record {model: crm.phonecall2phonecall, id: crm_phonecall2phonecall_interviewcall0}: + date: '2010-04-21 19:49:00' + name: Interview call + section_id: crm.section_sales_department + user_id: base.user_root +- | + I click on "Schedule" button of this wizard. +- + !python {model: crm.phonecall2phonecall}: | + self.action_apply(cr, uid, [ref('crm_phonecall2phonecall_interviewcall0')], {'active_id': ref('crm_phonecall_interviewcall0')}) diff --git a/addons/crm/wizard/__init__.py b/addons/crm/wizard/__init__.py index a9067d9fa0b..e4eb233a9b8 100644 --- a/addons/crm/wizard/__init__.py +++ b/addons/crm/wizard/__init__.py @@ -21,7 +21,6 @@ import crm_send_email import crm_forward_to_partner -import crm_email_add_cc import crm_lead_to_partner import crm_lead_to_opportunity diff --git a/addons/crm/wizard/crm_email_add_cc.py b/addons/crm/wizard/crm_email_add_cc.py deleted file mode 100644 index 9af1b5eda5d..00000000000 --- a/addons/crm/wizard/crm_email_add_cc.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-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 -from tools.translate import _ -import tools - -class crm_email_add_cc_wizard(osv.osv_memory): - """ Email Add CC""" - - _name = "crm.email.add.cc" - _description = "Email Add CC" - - _columns = { - 'name': fields.selection([('user', 'User'), ('partner', 'Partner'), \ - ('email', 'Email Address')], 'Send to', required=True), - 'user_id': fields.many2one('res.users', "User"), - 'partner_id': fields.many2one('res.partner', "Partner"), - 'email': fields.char('Email', size=32), - 'subject': fields.char('Subject', size=32), - } - - def on_change_email(self, cr, uid, ids, user, partner): - - """ @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of Mail’s IDs - """ - - if (not partner and not user): - return {'value': {'email': False}} - email = False - if partner: - addr = self.pool.get('res.partner').address_get(cr, uid, [partner], ['contact']) - if addr: - email = self.pool.get('res.partner.address').read(cr, uid, addr['contact'] , ['email'])['email'] - elif user: - addr = self.pool.get('res.users').read(cr, uid, user, ['address_id'])['address_id'] - if addr: - email = self.pool.get('res.partner.address').read(cr, uid, addr[0] , ['email'])['email'] - return {'value': {'email': email}} - - - def add_cc(self, cr, uid, ids, context={}): - """ - Adds CC value in case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of create menu’s IDs - @param context: A standard dictionary for contextual values - """ - - data = self.read(cr, uid, ids[0]) - email = data['email'] - subject = data['subject'] - - if not context: - return {} - history_line = self.pool.get('crm.case.history').browse(cr, uid, context['active_id']) - model = history_line.log_id.model_id.model - model_pool = self.pool.get(model) - case = model_pool.browse(cr, uid, history_line.log_id.res_id) - body = history_line.description.replace('\n', '\n> ') - if not case.email_from: - raise osv.except_osv(_('Warning!'), ('No email Address defined for this case..')) - if not email: - raise osv.except_osv(_('Warning!'), ('Please Specify email address..')) - flag = tools.email_send( - case.user_id.address_id.email, - [case.email_from], - subject or '[' + str(case.id) + '] ' + case.name, - model_pool.format_body(body), - email_cc = [email], - openobject_id = str(case.id), - subtype = "html" - ) - - if flag: - model_pool.write(cr, uid, case.id, {'email_cc' : case.email_cc and case.email_cc + ',' + email or email}) - else: - raise osv.except_osv(_('Email Fail!'), ("Lastest Email is not sent successfully")) - return {} - -crm_email_add_cc_wizard() - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/wizard/crm_email_add_cc_view.xml b/addons/crm/wizard/crm_email_add_cc_view.xml deleted file mode 100644 index 7c6ea96df23..00000000000 --- a/addons/crm/wizard/crm_email_add_cc_view.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - Add CC - crm.email.add.cc - form - -
- - - - - - - - - - - - - -