diff --git a/pingen/__init__.py b/pingen/__init__.py index e5f5183..ecfdbd0 100644 --- a/pingen/__init__.py +++ b/pingen/__init__.py @@ -20,4 +20,6 @@ ############################################################################## import ir_attachment +import pingen import pingen_task + diff --git a/pingen/__openerp__.py b/pingen/__openerp__.py index 314c473..e6d7d0e 100644 --- a/pingen/__openerp__.py +++ b/pingen/__openerp__.py @@ -49,6 +49,8 @@ Dependencies * Require the Python library `requests `_ * The PDF files sent to pingen.com have to respect some `formatting rules `_. + * The address must be in a format accepted by pingen.com: the last line + is the country. """, 'website': 'http://www.camptocamp.com', diff --git a/pingen/ir_attachment.py b/pingen/ir_attachment.py index 82558d2..4666930 100644 --- a/pingen/ir_attachment.py +++ b/pingen/ir_attachment.py @@ -19,6 +19,9 @@ # ############################################################################## +import requests +import base64 + from openerp.osv import osv, orm, fields from openerp.tools.translate import _ @@ -32,6 +35,19 @@ class ir_attachment(orm.Model): 'pingen_task_ids': fields.one2many( 'pingen.task', 'attachment_id', string="Pingen Task", readonly=True), + 'pingen_send': fields.boolean( + 'Send', + help="Defines if a document is merely uploaded or also sent"), + 'pingen_speed': fields.selection( + [('1', 'Priority'), ('2', 'Economy')], + 'Speed', + help="Defines the sending speed if the document is automatically sent"), + 'pingen_color': fields.selection( [('0', 'B/W'), ('1', 'Color')], 'Type of print'), + } + + _defaults = { + 'pingen_send': True, + 'pingen_speed': '2', } def _prepare_pingen_task_vals(self, cr, uid, attachment, context=None): @@ -88,3 +104,20 @@ class ir_attachment(orm.Model): self._handle_pingen_task(cr, uid, attachment_id, context=context) return res + def _decoded_content(self, cr, uid, attachment, context=None): + """ Returns the decoded content of an attachment (stored or url) + + Returns None if the type is 'url' and the url is not reachable. + """ + decoded_document = None + if attachment.type == 'binary': + decoded_document = base64.decodestring(attachment.datas) + elif attachment.type == 'url': + response = requests.get(attachment.url) + if response.ok: + decoded_document = requests.content + else: + raise Exception( + 'The type of attachment %s is not handled' % attachment.type) + return decoded_document + diff --git a/pingen/ir_attachment_view.xml b/pingen/ir_attachment_view.xml index e0a27e0..f13d9b7 100644 --- a/pingen/ir_attachment_view.xml +++ b/pingen/ir_attachment_view.xml @@ -11,6 +11,9 @@ + + + diff --git a/pingen/pingen.py b/pingen/pingen.py new file mode 100644 index 0000000..ab7ce56 --- /dev/null +++ b/pingen/pingen.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Guewen Baconnier +# Copyright 2012 Camptocamp SA +# +# 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 requests +import logging +import urlparse + +_logger = logging.getLogger(__name__) + + +class PingenException(RuntimeError): + """There was an ambiguous exception that occurred while handling your + request.""" + + +class ConnectionError(PingenException): + """An Error occured with the pingen API""" + + +class APIError(PingenException): + """An Error occured with the pingen API""" + + + +class Pingen(object): + """ Interface to pingen.com API + """ + + def __init__(self, token, staging=True): + self._token = token + self.staging = True + super(Pingen, self).__init__() + + @property + def url(self): + if self.staging: + return 'https://stage-api.pingen.com' + return 'https://api.pingen.com' + + def _send(self, method, endpoint, **kwargs): + """ Send a request to the pingen API using requests + + Add necessary boilerplate to call pingen.com API + (authentication, configuration, ...) + + :param boundmethod method: requests method to call + :param str endpoint: endpoint to call + :param kwargs: additional arguments forwarded to the requests method + """ + complete_url = urlparse.urljoin(self.url, endpoint) + + auth_param = {'token': self._token} + if 'params' in kwargs: + kwargs['params'].update(auth_param) + else: + kwargs['params'] = auth_param + + # with safe_mode, requests catch errors and + # returns a blank response with an error + config = {'safe_mode': True} + if 'config' in kwargs: + kwargs['config'].update(config) + else: + kwargs['config'] = config + + # verify = False required for staging environment + # because the SSL certificate is wrong + kwargs['verify'] = not self.staging + response = method(complete_url, **kwargs) + + if not response.ok: + raise ConnectionError(response.error) + + if response.json['error']: + raise APIError( + "%s: %s" % (response.json['errorcode'], response.json['errormessage'])) + + return response + + def push_document(self, document, send=None, speed=None, color=None): + """ Upload a document to pingen.com and eventually ask to send it + + :param tuple document: (filename, file stream) to push + :param boolean send: if True, the document will be sent by pingen.com + :param int/str speed: sending speed of the document if it is send + 1 = Priority, 2 = Economy + :param int/str color: type of print, 0 = B/W, 1 = Color + :return: tuple with 3 items: + 1. document_id on pingen.com + 2. post_id on pingen.com if it has been sent or None + 3. dict of the created item on pingen (details) + """ + document = {'file': document} + data = { + 'send': send, + 'speed': speed, + 'color': color, + } + response = self._send( + requests.post, + 'document/upload', + data=data, + files=document) + + json = response.json + + document_id = json['id'] + # confusing name but send_id is the posted id + posted_id = json.get('send', {}).get('send_id') + item = json['item'] + + return document_id, posted_id, item + + def send_document(self, document_id, speed=None, color=None): + """ Send a uploaded document to pingen.com + + :param int document_id: id of the document to send + :param int/str speed: sending speed of the document if it is send + 1 = Priority, 2 = Economy + :param int/str color: type of print, 0 = B/W, 1 = Color + :return: id of the post on pingen.com + """ + response = self._send( + requests.post, + 'document/send', + params={'id': document_id}) + + return response.json['id'] + diff --git a/pingen/pingen_task.py b/pingen/pingen_task.py index 771aa56..3e4b60d 100644 --- a/pingen/pingen_task.py +++ b/pingen/pingen_task.py @@ -19,17 +19,16 @@ # ############################################################################## -import requests -import urlparse -import base64 import logging +import datetime from cStringIO import StringIO from openerp.osv import orm, fields +from openerp import tools +from .pingen import Pingen, APIError, ConnectionError # TODO should be configurable -BASE_URL = 'https://stage-api.pingen.com' TOKEN = '6bc041af6f02854461ef31c2121ef853' _logger = logging.getLogger(__name__) @@ -48,28 +47,24 @@ class pingen_task(orm.Model): _columns = { 'attachment_id': fields.many2one( - 'ir.attachment', 'Document', required=True, ondelete='cascade'), + 'ir.attachment', 'Document', required=True, readonly=True, ondelete='cascade'), 'state': fields.selection( [('pending', 'Pending'), ('pushed', 'Pushed'), + ('sent', 'Sent'), ('error', 'Error'), + ('need_fix', 'Needs a correction'), ('canceled', 'Canceled')], string='State', readonly=True, required=True), - 'config': fields.text('Configuration (tmp)'), 'date': fields.datetime('Creation Date'), - 'push_date': fields.datetime('Pushed Date'), - 'send': fields.boolean( - 'Send', - help="Defines if a document is merely uploaed or also sent"), - 'speed': fields.selection( - [(1, 'Priority'), (2, 'Economy')], - 'Speed', - help="Defines the sending speed if the document is automatically sent"), - 'color': fields.selection( [(0, 'B/W'), (1, 'Color')], 'Type of print'), - 'last_error_code': fields.integer('Error Code', readonly=True), + 'push_date': fields.datetime('Push Date'), 'last_error_message': fields.text('Error Message', readonly=True), - 'pingen_id': fields.integer('Pingen ID'), - + 'pingen_id': fields.integer( + 'Pingen ID', + help="ID of the document in the Pingen Documents"), + 'post_id': fields.integer( + 'Pingen Post ID', + help="ID of the document in the Pingen Sendcenter"), } _defaults = { @@ -87,70 +82,47 @@ class pingen_task(orm.Model): """ - success = False - push_url = urlparse.urljoin(BASE_URL, 'document/upload') - auth = {'token': TOKEN} - - config = { - 'send': False, - 'speed': 2, - 'color': 1, - } - + attachment_obj = self.pool.get('ir.attachment') task = self.browse(cr, uid, task_id, context=context) - if task.type == 'binary': - decoded_document = base64.decodestring(task.datas) - else: - url_resp = requests.get(task.url) - if url_resp.status_code != 200: - task.write({'last_error_code': False, - 'last_error_message': "%s" % req.error, - 'state': 'error'}, - context=context) - return False - decoded_document = requests.content + decoded_document = attachment_obj._decoded_content( + cr, uid, task.attachment_id, context=context) - document = {'file': (task.datas_fname, StringIO(decoded_document))} + success = False + # parameterize + pingen = Pingen(TOKEN, staging=True) + doc = (task.datas_fname, StringIO(decoded_document)) try: - # TODO extract - req = requests.post( - push_url, - params=auth, - data=config, - files=document, - # verify = False required for staging environment - verify=False) - req.raise_for_status() - except (requests.HTTPError, - requests.Timeout, - requests.ConnectionError) as e: - msg = ('%s Message: %s. \n' - 'It occured when pushing the Pingen Task %s to pingen.com. \n' - 'It will be retried later.' % (e.__doc__, e, task.id)) - _logger.error(msg) + doc_id, post_id, __ = pingen.push_document( + doc, task.pingen_send, task.pingen_speed, task.pingen_color) + except ConnectionError as e: + # we can continue and it will be retried the next time + _logger.exception('Connection Error when pushing Pingen Task %s to %s.' % + (task.id, pingen.url)) task.write( - {'last_error_code': False, - 'last_error_message': msg, + {'last_error_message': e, 'state': 'error'}, context=context) - else: - response = req.json - if response['error']: - task.write( - {'last_error_code': response['errorcode'], - 'last_error_message': 'Error from pingen.com: %s' % response['errormessage'], - 'state': 'error'}, - context=context) - else: - task.write( - {'last_error_code': False, - 'last_error_message': False, - 'state': 'pushed', - 'pingen_id': response['id'],}, + + except APIError as e: + _logger.warning('API Error when pushing Pingen Task %s to %s.' % + (task.id, pingen.url)) + task.write( + {'last_error_message': e, + 'state': 'need_fix'}, context=context) - _logger.info('Pingen Task %s pushed to pingen.com.' % task.id) - success = True + else: + success = True + + now = datetime.datetime.now().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + task.write( + {'last_error_message': False, + 'state': 'pushed', + 'push_date': now, + 'pingen_id': doc_id, + 'post_id': post_id}, + context=context) + _logger.info('Pingen Task %s pushed to %s' % (task.id, pingen.url)) return success @@ -163,10 +135,3 @@ class pingen_task(orm.Model): self._push_to_pingen(cr, uid, task_id, context=context) return True - - # r = requests.get(push_url, params=auth, verify=False) - - - # TODO: resolve = put in pending or put in pushed - - diff --git a/pingen/pingen_task_view.xml b/pingen/pingen_task_view.xml index 1aeea82..0ba7d69 100644 --- a/pingen/pingen_task_view.xml +++ b/pingen/pingen_task_view.xml @@ -8,9 +8,12 @@ tree - - - + + + + + + @@ -21,11 +24,65 @@ form
- - - - -