diff --git a/calendar_ics/__init__.py b/calendar_ics/__init__.py new file mode 100644 index 0000000..071a504 --- /dev/null +++ b/calendar_ics/__init__.py @@ -0,0 +1,2 @@ +import calendar +import res_partner diff --git a/calendar_ics/__init__.pyc b/calendar_ics/__init__.pyc new file mode 100644 index 0000000..b33230b Binary files /dev/null and b/calendar_ics/__init__.pyc differ diff --git a/calendar_ics/__openerp__.py b/calendar_ics/__openerp__.py new file mode 100644 index 0000000..d31a300 --- /dev/null +++ b/calendar_ics/__openerp__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution, third party addon +# Copyright (C) 2004-2016 Vertel AB (). +# +# 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 . +# +############################################################################## + +{ + 'name': 'Calendar ics-urls', + 'version': '0.1', + 'category': 'Tools', + 'summary': 'Subscription on calendar.ics-urls', + 'licence': 'AGPL-3', + 'description': """ +Adds and updates calendar objects according to an ics-url + +""", + 'author': 'Vertel AB', + 'website': 'http://www.vertel.se', + 'depends': ['calendar',], + 'data': [ 'res_partner_view.xml', + #'security/ir.model.access.csv', + 'res_partner_data.xml' + ], + 'application': False, + 'installable': True, + 'demo': ['calendar_ics_demo.xml',], +} +# vim:expandtab:smartindent:tabstop=4s:softtabstop=4:shiftwidth=4: diff --git a/calendar_ics/calendar.py b/calendar_ics/calendar.py new file mode 100644 index 0000000..3970b1f --- /dev/null +++ b/calendar_ics/calendar.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution, third party addon +# Copyright (C) 2004-2016 Vertel AB (). +# +# 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 openerp import models, fields, api, _ +from pytz import timezone +from openerp.exceptions import except_orm, Warning, RedirectWarning +from datetime import datetime, timedelta, time +from time import strptime, mktime, strftime +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +import re + +from openerp import http +from openerp.http import request + +import logging +_logger = logging.getLogger(__name__) + + +try: + from icalendar import Calendar, Event, vDatetime, FreeBusy +except ImportError: + raise Warning('icalendar library missing, pip install icalendar') + +try: + import urllib2 +except ImportError: + raise Warning('urllib2 library missing, pip install urllib2') + + +# calendar_ics -> res.partner + +# http://ical.oops.se/holidays/Sweden/-1,+1 +# http://www.skatteverketkalender.se/skvcal-manadsmoms-maxfyrtiomiljoner-ingenperiodisk-ingenrotrut-verk1.ics + +class calendar_event(models.Model): + _inherit = 'calendar.event' + + ics_subscription = fields.Boolean(default=False) # partner_ids + ics_subscription -> its ok to delete + + @api.multi + def set_ics_event(self, ics_file, partner): + for event in Calendar.from_ical(ics_file).walk('vevent'): + #~ if not event.get('uid'): + #~ event.add('uid',reduce(lambda x,y: x ^ y, map(ord, str(event.get('dtstart') and event.get('dtstart').dt or '' + event.get('summary') + event.get('dtend') and event.get('dtend').dt or ''))) % 1024) + + summary = '' + description = unicode(event.get('description', '')) + if unicode(event.get('summary')) and len(unicode(event.get('summary'))) < 35: + summary = unicode(event.get('summary')) + elif len(unicode(event.get('summary'))) >= 35: + summary = unicode(event.get('summary'))[:35] + if not event.get('description'): + description = unicode(event.get('summary')) + + record = {r[1]:r[2] for r in [ ('dtstart','start_date',event.get('dtstart') and event.get('dtstart').dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + ('dtend','stop_date',event.get('dtend') and event.get('dtend').dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + #~ ('dtstamp','start_datetime',event.get('dtstamp') and event.get('dtstamp').dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + #~ ('description','description',description), + ('duration','duration',event.get('duration')), + ('location','location',event.get('location') and unicode(event.get('location')) or partner.ics_location), + ('class','class',event.get('class') and str(event.get('class')) or partner.ics_class), + ('summary','name',summary), + ('rrule', 'rrule',event.get('rrule') and event.get('rrule').to_ical() or None), + ] if event.get(r[0])} + + partner_ids = self.env['res.partner'].get_attendee_ids(event) + #~ raise Warning(partner_ids) + if partner_ids: + partner_ids.append(partner.id) + else: + partner_ids = [partner.id] + + record['partner_ids'] = [(6,0,[partner_ids])] + #~ record['partner_ids'] = [(6,0,self.env['res.partner'].get_attendee_ids(event)[0] and self.env['res.partner'].get_attendee_ids(event)[0].append(partner.id) or [partner.id])] + #~ raise Warning(record['partner_ids']) + #~ record['attendee_ids'] = [(6,0,[attendee])] + record['ics_subscription'] = True + record['start'] = record.get('start_date') + record['stop'] = record.get('stop_date') or record.get('start') + record['description'] = description + record['show_as'] = partner.ics_show_as + record['allday'] = partner.ics_allday + #~ record['rrule'] = event.get('rrule').to_ical() + #~ raise Warning(record['rrule_type'].to_ical) + + tmpStart = datetime.time(datetime.fromtimestamp(mktime(strptime(record['start'], DEFAULT_SERVER_DATETIME_FORMAT)))) + tmpStop = datetime.fromtimestamp(mktime(strptime(record['stop'], DEFAULT_SERVER_DATETIME_FORMAT))) + + if tmpStart == time(0,0,0) and tmpStart == datetime.time(tmpStop): + record['allday'] = True + + if not record.get('stop_date'): + record['allday'] = True + record['stop_date'] = record['start_date'] + elif record.get('stop_date') and record['allday']: + record['stop_date'] = vDatetime(tmpStop - timedelta(hours=24)).dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + record['stop'] = record['stop_date'] + _logger.error('ICS %s' % record) + self.env['calendar.event'].create(record) + #~ event_id = self.env['calendar.event'].create(record) +#~ + #~ attendee_values = self.env['res.partner'].get_attendee_ids(event) + #~ for i in range(len(attendee_values[0])): + #~ self.env['calendar.attendee'].create({ + #~ 'event_id': event_id.id, + #~ 'partner_id': attendee_values[0][i], + #~ 'email': attendee_values[1][i], + #~ }) + + #~ 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="Status of the attendee's participation"), + #~ 'cn': fields.function(_compute_data, string='Common name', type="char", multi='cn', store=True), + #~ 'partner_id': fields.many2one('res.partner', 'Contact', readonly="True"), + #~ 'email': fields.char('Email', help="Email of Invited Person"), + #~ 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"), + #~ 'access_token': fields.char('Invitation Token'), + #~ 'event_id': fields.many2one('calendar.event', 'Meeting linked', ondelete='cascade'), + + @api.multi + def get_ics_event(self): + event = self[0] + ics = Event() + ics = self.env['calendar.attendee'].get_ics_file(event) + calendar = Calendar() + date_format = DEFAULT_SERVER_DATETIME_FORMAT + + + #~ for t in ics_record: + #~ ics[t[2]] = eval(t[3]) + #~ + #~ foo = {ics[t[2]]: event.read([t[1]]) for t in ics_record} + #~ + #~ + #~ ics['uid'] = event.id + #~ ics['allday'] = event.allday + #~ + #~ if ics['allday']: + #~ date_format = DEFAULT_SERVER_DATE_FORMAT + #~ + #~ ics['dtstart'] = vDatetime(datetime.fromtimestamp(mktime(strptime(event.start_date, date_format)))) + #~ ics['dtend'] = vDatetime(datetime.fromtimestamp(mktime(strptime(event.stop_date, date_format)))) + #~ ics['summary'] = event.name + #~ ics['description'] = event.description + #~ ics['class'] = event.read(['class']) + + #~ calendar.add_component(ics) + #~ raise Warning(calendar.to_ical()) + return ics + + + @api.multi + def get_ics_file(self, events_exported, partner): + """ + Returns iCalendar file for the event invitation. + @param event: event object (browse record) + @return: .ics file content + """ + ics = Event() + event = self[0] + + #~ raise Warning(self.env.cr.dbname) + #~ The method below needs som proper rewriting to avoid overusing libraries. + def ics_datetime(idate, allday=False): + if idate: + if allday: + return str(vDatetime(datetime.fromtimestamp(mktime(strptime(idate, DEFAULT_SERVER_DATETIME_FORMAT)))).to_ical())[:8] + else: + return vDatetime(datetime.fromtimestamp(mktime(strptime(idate, DEFAULT_SERVER_DATETIME_FORMAT)))).to_ical() + 'Z' + return False + + #~ try: + #~ # FIXME: why isn't this in CalDAV? + #~ import vobject + #~ except ImportError: + #~ return res + + #~ cal = vobject.iCalendar() + + #~ event = cal.add('vevent') + if not event.start or not event.stop: + raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation.")) + ics['summary'] = event.name + if event.description: + ics['description'] = event.description + if event.location: + ics['location'] = event.location + if event.rrule: + ics['rrule'] = event.rrule + #~ ics.add('rrule', str(event.rrule), encode=0) + #~ raise Warning(ics['rrule']) + + if event.alarm_ids: + for alarm in event.alarm_ids: + valarm = ics.add('valarm') + interval = alarm.interval + duration = alarm.duration + trigger = valarm.add('TRIGGER') + trigger.params['related'] = ["START"] + if interval == 'days': + delta = timedelta(days=duration) + elif interval == 'hours': + delta = timedelta(hours=duration) + elif interval == 'minutes': + delta = timedelta(minutes=duration) + trigger.value = delta + valarm.add('DESCRIPTION').value = alarm.name or 'Odoo' + if event.attendee_ids: + for attendee in event.attendee_ids: + attendee_add = ics.get('attendee') + attendee_add = attendee.cn and ('CN=' + attendee.cn) or '' + if attendee.cn and attendee.email: + attendee_add += ':' + attendee_add += attendee.email and ('MAILTO:' + attendee.email) or '' + + ics.add('attendee', attendee_add, encode=0) + + if events_exported: + event_not_found = True + + for event_comparison in events_exported: + #~ raise Warning('event_comparison = %s ics = %s' % (event_comparison, ics)) + if str(ics) == event_comparison: + event_not_found = False + break + + if event_not_found: + events_exported.append(str(ics)) + + ics['uid'] = '%s@%s-%s' % (event.id, self.env.cr.dbname, partner.id) + ics['created'] = ics_datetime(strftime(DEFAULT_SERVER_DATETIME_FORMAT)) + tmpStart = ics_datetime(event.start, event.allday) + tmpEnd = ics_datetime(event.stop, event.allday) + + if event.allday: + ics['dtstart;value=date'] = tmpStart + else: + ics['dtstart'] = tmpStart + + if tmpStart != tmpEnd or not event.allday: + if event.allday: + ics['dtend;value=date'] = str(vDatetime(datetime.fromtimestamp(mktime(strptime(event.stop, DEFAULT_SERVER_DATETIME_FORMAT))) + timedelta(hours=24)).to_ical())[:8] + else: + ics['dtend'] = tmpEnd + + return [ics, events_exported] + + else: + events_exported.append(str(ics)) + + ics['uid'] = '%s@%s-%s' % (event.id, self.env.cr.dbname, partner.id) + ics['created'] = ics_datetime(strftime(DEFAULT_SERVER_DATETIME_FORMAT)) + tmpStart = ics_datetime(event.start, event.allday) + tmpEnd = ics_datetime(event.stop, event.allday) + + if event.allday: + ics['dtstart;value=date'] = tmpStart + else: + ics['dtstart'] = tmpStart + + if tmpStart != tmpEnd or not event.allday: + if event.allday: + ics['dtend;value=date'] = str(vDatetime(datetime.fromtimestamp(mktime(strptime(event.stop, DEFAULT_SERVER_DATETIME_FORMAT))) + timedelta(hours=24)).to_ical())[:8] + else: + ics['dtend'] = tmpEnd + + return [ics, events_exported] + + @api.multi + def get_ics_freebusy(self): + """ + Returns iCalendar file for the event invitation. + @param event: event object (browse record) + @return: .ics file content + """ + #~ ics = FreeBusy() + event = self[0] + + def ics_datetime(idate, iallday=False): + if idate: + return vDatetime(idate).to_ical() + return False + + if not event.start or not event.stop: + raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation.")) + + allday = event.allday + event_start = datetime.fromtimestamp(mktime(strptime(event.start, DEFAULT_SERVER_DATETIME_FORMAT))) + event_stop = datetime.fromtimestamp(mktime(strptime(event.stop, DEFAULT_SERVER_DATETIME_FORMAT))) + + if allday: + event_stop += timedelta(hours=23, minutes=59, seconds=59) + + return '%s/%s' % (ics_datetime(event_start, allday), ics_datetime(event_stop, allday)) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/calendar_ics/calendar.pyc b/calendar_ics/calendar.pyc new file mode 100644 index 0000000..926898d Binary files /dev/null and b/calendar_ics/calendar.pyc differ diff --git a/calendar_ics/calendar_ics_demo.xml b/calendar_ics/calendar_ics_demo.xml new file mode 100644 index 0000000..6673e43 --- /dev/null +++ b/calendar_ics/calendar_ics_demo.xml @@ -0,0 +1,76 @@ + + + + + + + + Demo meeting about meat + Meeting to discuss project plan and hash out the details of implementation. + + + + + + open + + + + + + + Demo Kickoff + Weekend at burneys + + + + + + open + + + + + + + Demo Bring the meat. + All you can eat buffe. + + + + + + open + + + + + + + Demo Trash the meat grinder + Bring your own hammers and tools + BurgerTown, 31 butcherstreet + + + + + + open + + + + + + + Demo Trash the meat grinder reunion! + Bring your own hammers and tools AND meat this time + BurgerTown, 31 butcherstreet + + + + + + open + + + diff --git a/calendar_ics/calendar_view.xml b/calendar_ics/calendar_view.xml new file mode 100644 index 0000000..b8eaa0b --- /dev/null +++ b/calendar_ics/calendar_view.xml @@ -0,0 +1,108 @@ + + + + + + + + + Calendar - ics url + calendar.ics + +
+ +
+
+
+

+ +

+

+ + + +

+
+
+
+ + +
+
+
+
+ + + + + + Calendar ics-url + calendar.ics + + + + + + +