diff --git a/addons/.bzrignore b/addons/.bzrignore index 8d98f9debde..5fee3e96f5d 100644 --- a/addons/.bzrignore +++ b/addons/.bzrignore @@ -1 +1,5 @@ .* +pad/__init__.pyc +pad/ir_attachment.pyc +pad/res_company.pyc +web_etherpad diff --git a/addons/pad/__init__.py b/addons/pad/__init__.py index 663cac2cbf0..ee80bcaf8d0 100644 --- a/addons/pad/__init__.py +++ b/addons/pad/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import pad import res_company -import ir_attachment + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad/__openerp__.py b/addons/pad/__openerp__.py index 267f595c7e5..bc4159089d7 100644 --- a/addons/pad/__openerp__.py +++ b/addons/pad/__openerp__.py @@ -25,6 +25,7 @@ Lets the company customize which Pad installation should be used to link to new "static/src/xml/*.xml", ], 'images': ['static/src/img/pad_link_companies.jpeg'], + "css": ['static/src/css/etherpad.css',], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad/etherpad.py b/addons/pad/etherpad.py new file mode 100644 index 00000000000..547720c1cc8 --- /dev/null +++ b/addons/pad/etherpad.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python +"""Module to talk to EtherpadLite API.""" + +import json +import urllib +import urllib2 + + +class EtherpadLiteClient: + """Client to talk to EtherpadLite API.""" + API_VERSION = 1 # TODO probably 1.1 sometime soon + + CODE_OK = 0 + CODE_INVALID_PARAMETERS = 1 + CODE_INTERNAL_ERROR = 2 + CODE_INVALID_FUNCTION = 3 + CODE_INVALID_API_KEY = 4 + TIMEOUT = 20 + + apiKey = "" + baseUrl = "" + + def __init__(self, apiKey=None, baseUrl=None): + if apiKey: + self.apiKey = apiKey + + if baseUrl: + self.baseUrl = baseUrl + + def call(self, function, arguments=None): + """Create a dictionary of all parameters""" + url = '%s/%d/%s' % (self.baseUrl, self.API_VERSION, function) + + params = arguments or {} + params.update({'apikey': self.apiKey}) + data = urllib.urlencode(params, True) + + try: + opener = urllib2.build_opener() + request = urllib2.Request(url=url, data=data) + response = opener.open(request, timeout=self.TIMEOUT) + result = response.read() + response.close() + except urllib2.HTTPError: + raise + + result = json.loads(result) + if result is None: + raise ValueError("JSON response could not be decoded") + + return self.handleResult(result) + + def handleResult(self, result): + """Handle API call result""" + if 'code' not in result: + raise Exception("API response has no code") + if 'message' not in result: + raise Exception("API response has no message") + + if 'data' not in result: + result['data'] = None + + if result['code'] == self.CODE_OK: + return result['data'] + elif result['code'] == self.CODE_INVALID_PARAMETERS or result['code'] == self.CODE_INVALID_API_KEY: + raise ValueError(result['message']) + elif result['code'] == self.CODE_INTERNAL_ERROR: + raise Exception(result['message']) + elif result['code'] == self.CODE_INVALID_FUNCTION: + raise Exception(result['message']) + else: + raise Exception("An unexpected error occurred whilst handling the response") + + # GROUPS + # Pads can belong to a group. There will always be public pads that do not belong to a group (or we give this group the id 0) + + def createGroup(self): + """creates a new group""" + return self.call("createGroup") + + def createGroupIfNotExistsFor(self, groupMapper): + """this functions helps you to map your application group ids to etherpad lite group ids""" + return self.call("createGroupIfNotExistsFor", { + "groupMapper": groupMapper + }) + + def deleteGroup(self, groupID): + """deletes a group""" + return self.call("deleteGroup", { + "groupID": groupID + }) + + def listPads(self, groupID): + """returns all pads of this group""" + return self.call("listPads", { + "groupID": groupID + }) + + def createGroupPad(self, groupID, padName, text=''): + """creates a new pad in this group""" + params = { + "groupID": groupID, + "padName": padName, + } + if text: + params['text'] = text + return self.call("createGroupPad", params) + + # AUTHORS + # Theses authors are bind to the attributes the users choose (color and name). + + def createAuthor(self, name=''): + """creates a new author""" + params = {} + if name: + params['name'] = name + return self.call("createAuthor", params) + + def createAuthorIfNotExistsFor(self, authorMapper, name=''): + """this functions helps you to map your application author ids to etherpad lite author ids""" + params = { + 'authorMapper': authorMapper + } + if name: + params['name'] = name + return self.call("createAuthorIfNotExistsFor", params) + + # SESSIONS + # Sessions can be created between a group and a author. This allows + # an author to access more than one group. The sessionID will be set as + # a cookie to the client and is valid until a certain date. + + def createSession(self, groupID, authorID, validUntil): + """creates a new session""" + return self.call("createSession", { + "groupID": groupID, + "authorID": authorID, + "validUntil": validUntil + }) + + def deleteSession(self, sessionID): + """deletes a session""" + return self.call("deleteSession", { + "sessionID": sessionID + }) + + def getSessionInfo(self, sessionID): + """returns informations about a session""" + return self.call("getSessionInfo", { + "sessionID": sessionID + }) + + def listSessionsOfGroup(self, groupID): + """returns all sessions of a group""" + return self.call("listSessionsOfGroup", { + "groupID": groupID + }) + + def listSessionsOfAuthor(self, authorID): + """returns all sessions of an author""" + return self.call("listSessionsOfAuthor", { + "authorID": authorID + }) + + # PAD CONTENT + # Pad content can be updated and retrieved through the API + + def getText(self, padID, rev=None): + """returns the text of a pad""" + params = {"padID": padID} + if rev is not None: + params['rev'] = rev + return self.call("getText", params) + + # introduced with pull request merge + def getHtml(self, padID, rev=None): + """returns the html of a pad""" + params = {"padID": padID} + if rev is not None: + params['rev'] = rev + return self.call("getHTML", params) + + def setText(self, padID, text): + """sets the text of a pad""" + return self.call("setText", { + "padID": padID, + "text": text + }) + + def setHtml(self, padID, html): + """sets the text of a pad from html""" + return self.call("setHTML", { + "padID": padID, + "html": html + }) + + # PAD + # Group pads are normal pads, but with the name schema + # GROUPID$PADNAME. A security manager controls access of them and its + # forbidden for normal pads to include a in the name. + + def createPad(self, padID, text=''): + """creates a new pad""" + params = { + "padID": padID, + } + if text: + params['text'] = text + return self.call("createPad", params) + + def getRevisionsCount(self, padID): + """returns the number of revisions of this pad""" + return self.call("getRevisionsCount", { + "padID": padID + }) + + def deletePad(self, padID): + """deletes a pad""" + return self.call("deletePad", { + "padID": padID + }) + + def getReadOnlyID(self, padID): + """returns the read only link of a pad""" + return self.call("getReadOnlyID", { + "padID": padID + }) + + def setPublicStatus(self, padID, publicStatus): + """sets a boolean for the public status of a pad""" + return self.call("setPublicStatus", { + "padID": padID, + "publicStatus": publicStatus + }) + + def getPublicStatus(self, padID): + """return true of false""" + return self.call("getPublicStatus", { + "padID": padID + }) + + def setPassword(self, padID, password): + """returns ok or a error message""" + return self.call("setPassword", { + "padID": padID, + "password": password + }) + + def isPasswordProtected(self, padID): + """returns true or false""" + return self.call("isPasswordProtected", { + "padID": padID + }) + diff --git a/addons/pad/ir_attachment.py b/addons/pad/ir_attachment.py deleted file mode 100644 index ba8c88632dd..00000000000 --- a/addons/pad/ir_attachment.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -from osv import fields, osv -import random -import string - -class ir_attachment(osv.osv): - _inherit = 'ir.attachment' - - def pad_generate_url(self, cr, uid, model, id): - pad_url_template = self.pool.get('res.users').browse(cr,uid,[uid])[0].company_id.pad_url_template - s = string.ascii_uppercase + string.digits - salt = ''.join([s[random.randint(0, len(s) - 1)] for i in range(8)]) - template_vars = { - 'db' : cr.dbname, - 'model' : model, - 'id' : id, - 'salt' : salt, - 'name' : '', - } - return pad_url_template % template_vars - - def pad_get(self, cr, uid, model, id): - attachment = self.search(cr, uid, [('res_model', '=', model), ('res_id', '=', id), ('type', '=', 'url'), ('name', '=', 'Pad')]) - if attachment: - return self.read(cr, uid, attachment)[0]['url'] - else: - url = self.pad_generate_url(cr, uid, model, id) - self.create(cr, uid, { - 'res_model' : model, - 'res_id' : id, - 'type' : 'url', - 'name' : 'Pad', - 'url' : url, - }) - return url - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad/pad.py b/addons/pad/pad.py new file mode 100644 index 00000000000..c7e448767bc --- /dev/null +++ b/addons/pad/pad.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from osv import fields, osv +import random +import string +from etherpad import EtherpadLiteClient +import urllib2 +from tools.translate import _ + + +class pad_common(osv.osv_memory): + _name = 'pad.common' + _pad_url = None # name of the field for the etherpad + def pad_generate_url(self, cr, uid, model, context=None): + pad_url_template = self._pad_url_template(cr, uid, context) + s = string.ascii_uppercase + string.digits + salt = ''.join([s[random.randint(0, len(s) - 1)] for i in range(8)]) + template_vars = { + 'db' : cr.dbname, + 'model' : model, + 'salt' : salt, + } + url = pad_url_template % template_vars + api_key = self._pad_api_key(cr, uid, context) + if api_key: + urls = url.split('/') + api_url = '/'.join(urls[:3]) + "/api" + pad_id = urls[-1] + ep_client = EtherpadLiteClient(api_key, api_url) + try: + ep_client.createPad(pad_id," ") + except ValueError as strerror: + raise osv.except_osv(_('Configuration Error !'),_("Etherpad Have Wrong API Key.")) + except urllib2.HTTPError as e: + raise osv.except_osv(_('Configuration Error !'),_("Etherpad Have Wrong API URL.")) + except urllib2.URLError as e: + raise osv.except_osv(_('Configuration Error !'),_("Etherpad Have Wrong Pad URL Template.")) + return url + + def _pad_api_key(self, cr, uid, context=None): + return self.pool.get('res.users').browse(cr,uid, uid, context).company_id.etherpad_api_key + + def _pad_url_template(self, cr, uid, context=None): + return self.pool.get('res.users').browse(cr,uid, uid, context).company_id.pad_url_template + + def copy(self, cr, uid, id, default=None, context=None): + if not default: + default = {} + default.update({ + self._pad_url:self.pad_generate_url(cr, uid, self._name), + }) + return super(pad_common, self).copy(cr, uid, id, default, context) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad/res_company.py b/addons/pad/res_company.py index 0c1dc7c4929..4ce27fd4d22 100644 --- a/addons/pad/res_company.py +++ b/addons/pad/res_company.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- from osv import fields, osv +PAD_TEMPLATE = 'http://beta.etherpad.org/p/%(db).10s-%(model)s-%(salt)s' +PAD_API_KEY = 'EtherpadFTW' + class company_pad(osv.osv): _inherit = 'res.company' _columns = { 'pad_url_template': fields.char('Pad URL Template', size=128, required=True, help="Template used to generate pad URL."), + 'etherpad_api_key': fields.char('Pad API Key', size=128), } _defaults = { - 'pad_url_template': 'http://ietherpad.com/%(db)s-%(model)s-%(id)d-%(salt)s-%(name)s' + 'pad_url_template': PAD_TEMPLATE, + 'etherpad_api_key': PAD_API_KEY, } diff --git a/addons/pad/res_company.xml b/addons/pad/res_company.xml index 9b1c0656470..96c3c004bef 100644 --- a/addons/pad/res_company.xml +++ b/addons/pad/res_company.xml @@ -9,6 +9,7 @@ + diff --git a/addons/pad/static/src/css/etherpad.css b/addons/pad/static/src/css/etherpad.css new file mode 100644 index 00000000000..fd804c95472 --- /dev/null +++ b/addons/pad/static/src/css/etherpad.css @@ -0,0 +1,71 @@ +.etherpad_head{ + width: 99.7%; + display: block; + border-radius: 3px 3px 0px 0px; + background: -webkit-linear-gradient( #F7F7F7, #F1F1F1 80%); + padding-left:3px; + cursor:pointer; +} +.etherpad_zoom_head{ + position: fixed; + left: 0px; + top: 0px; + width: 100%; + height: 15px; +} +.etherpad_zoom{ + position: fixed; + left: 0px; + top: 15px; + width: 100%; + height: 98% !important; + z-index:10001 !important; + background-color:white; +} +.etherpad_default{ + width: 100%; + height: 450px; +} +.etherpad_body{ + overflow:hidden; +} + +.etherpad_readonly{ + width: 97.2%; + height: 450px; + padding:7px 7px 10px 10px; + overflow:auto; +} +.etherpad_readonly ul,.etherpad_readonly ol{ + -webkit-margin-before: 1em; + -webkit-margin-after: 1em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px !important; +} +.etherpad_readonly ul li{ + list-style-type: disc; +} +.etherpad_readonly ol li{ + list-style-type: decimal; +} +.etherpad_readonly .indent li{ + list-style-type: none !important; +} + + +.etherpad_readonly{ + font-family: arial, sans-serif; + font-size: 13px; + line-height: 17px; +} + +.etherpad_readonly ul.indent { list-style-type: none !important; } +.etherpad_readonly ol li{ list-style-type: decimal !important; } +.etherpad_readonly ol ol li{ list-style-type: lower-latin !important; } +.etherpad_readonly ol ol ol li{ list-style-type: lower-roman !important; } +.etherpad_readonly ol ol ol ol li{ list-style-type: decimal !important; } +.etherpad_readonly ol ol ol ol ol li{ list-style-type: lower-latin !important; } +.etherpad_readonly ol ol ol ol ol ol li{ list-style-type: lower-roman !important; } +.etherpad_readonly ol ol ol ol ol ol ol li{ list-style-type: decimal !important; } +.etherpad_readonly ol ol ol ol ol ol ol ol li{ list-style-type: lower-latin !important; } diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index 2e5f8820ac4..3f13df45d48 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -1,18 +1,41 @@ openerp.pad = function(instance) { -instance.web.Sidebar = instance.web.Sidebar.extend({ - init: function(parent) { - this._super(parent); - this.add_items('other',[{ label: "Pad", callback: this.on_add_pad }]); - }, - on_add_pad: function() { - var self = this; - var view = this.getParent(); - var model = new instance.web.Model('ir.attachment'); - model.call('pad_get', [view.model ,view.datarecord.id],{}).then(function(r) { - self.do_action({ type: "ir.actions.act_url", url: r }); - }); - } -}); - -}; +instance.web.form.FieldEtherpad = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { + template: 'FieldEtherpad', + initialize_content: function() { + this.$textarea = undefined; + this.$element.find('span').text(this.field.string); + this.$element.find('span').click(_.bind(function(ev){ + this.$element.find('span').toggleClass('etherpad_zoom_head'); + var iszoom = this.$element.find('span').hasClass('etherpad_zoom_head'); + this.$element.find('span').text((iszoom?'Back to Task':this.field.string)); + this.$element.find('div').toggleClass('etherpad_zoom'); + $("body").toggleClass('etherpad_body'); + },this)); + }, + set_value: function(value_) { + this._super(value_); + this.render_value(); + }, + render_value: function() { + var show_value = instance.web.format_value(this.get('value'), this, ''); + if (!this.get("effective_readonly")) { + var pad_username = this.session.username; + this.$element.find('div').html(''); + } else { + if(this.get('value') != false) + { + var self = this; + if(show_value.split('\n')[0] != '') + $.get(show_value.split('\n')[0]+'/export/html') + .success(function(data) { self.$element.html('
'+data+'
'); }) + .error(function() { self.$element.text('Unable to load pad'); }); + } + } + }, + })); + + instance.web.form.widgets = instance.web.form.widgets.extend({ + 'etherpad': 'instance.web.form.FieldEtherpad', + }); +}; diff --git a/addons/pad/static/src/xml/pad.xml b/addons/pad/static/src/xml/pad.xml index e922294d03d..334064e88a1 100644 --- a/addons/pad/static/src/xml/pad.xml +++ b/addons/pad/static/src/xml/pad.xml @@ -2,4 +2,14 @@ + + + +
+ +
+
+
+
+
diff --git a/addons/pad_project/__init__.py b/addons/pad_project/__init__.py index d6bf9f9551c..098de31a5a4 100644 --- a/addons/pad_project/__init__.py +++ b/addons/pad_project/__init__.py @@ -19,6 +19,6 @@ # ############################################################################## -import models +import project_task # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad_project/__openerp__.py b/addons/pad_project/__openerp__.py index 8ca761d2737..61ebfa8f17f 100644 --- a/addons/pad_project/__openerp__.py +++ b/addons/pad_project/__openerp__.py @@ -31,9 +31,9 @@ This module adds a PAD in all project kanban views 'website': 'http://www.openerp.com', 'depends': ['project', 'pad'], 'init_xml': [], - 'update_xml': ['models/project_task.xml'], + 'update_xml': ['project_task.xml'], 'demo_xml': [], 'installable': True, - 'auto_install': False, + 'auto_install': True, } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad_project/models/__init__.py b/addons/pad_project/models/__init__.py deleted file mode 100644 index 098de31a5a4..00000000000 --- a/addons/pad_project/models/__init__.py +++ /dev/null @@ -1,24 +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 . -# -############################################################################## - -import project_task - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad_project/models/project_task.py b/addons/pad_project/models/project_task.py deleted file mode 100644 index c215aed1325..00000000000 --- a/addons/pad_project/models/project_task.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from tools.translate import _ -from osv import fields, osv - -class task(osv.osv): - _inherit = "project.task" - - def pad_get(self, cr, uid, ids, context=None): - """Get pad action - """ - url = self.pool.get("ir.attachment").pad_get(cr, uid, self._name, ids[0]) - return { - 'type': 'ir.actions.act_url', - 'url': url - } - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pad_project/models/project_task.xml b/addons/pad_project/models/project_task.xml deleted file mode 100644 index 3c734cf3b97..00000000000 --- a/addons/pad_project/models/project_task.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - project.task.kanban.pad - project.task - kanban - - - - PAD - - - - - diff --git a/addons/pad_project/project_task.py b/addons/pad_project/project_task.py new file mode 100644 index 00000000000..96e1ef86e4d --- /dev/null +++ b/addons/pad_project/project_task.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from tools.translate import _ +from osv import fields, osv + +class task(osv.osv): + _name = "project.task" + _inherit = ["project.task",'pad.common'] + _pad_url = 'description' + _defaults = { + _pad_url: lambda self, cr, uid, context: self.pad_generate_url(cr, uid, self._name, context), + } diff --git a/addons/pad_project/project_task.xml b/addons/pad_project/project_task.xml new file mode 100644 index 00000000000..da9a9b2c3a1 --- /dev/null +++ b/addons/pad_project/project_task.xml @@ -0,0 +1,18 @@ + + + + project.task.form.pad + project.task + form + + + + + + + + +