view src/plugins/plugin_misc_tickets.py @ 2447:9e692f09f367

plugin tickets: handle "update" flag + various improvments: - ticketsSet renamed to ticketSet as only one ticket is set at a time - dateFilter is not crashing anymore if value can't be parsed: the value is then return unmodified - form_ns can be specified in get, so the method can be used by other plugins - new "update" flag: if set in extra, an previous ticket of the same id will be retrieved, and used as a basis for an update
author Goffi <goffi@goffi.org>
date Thu, 30 Nov 2017 20:34:41 +0100
parents 81a45e7886c9
children 544c4d2fec45
line wrap: on
line source

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

# SAT plugin for Pubsub Schemas
# Copyright (C) 2009-2017 Jérôme Poisson (goffi@goffi.org)

# 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 <http://www.gnu.org/licenses/>.

from sat.core.i18n import _
from sat.core.constants import Const as C
from sat.core import exceptions
from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from wokkel import generic
from sat.tools import utils
from sat.tools.common import uri
from sat.core.log import getLogger
import shortuuid
from wokkel import data_form
log = getLogger(__name__)

NS_TICKETS = 'org.salut-a-toi.tickets:0'

PLUGIN_INFO = {
    C.PI_NAME: _("Tickets management"),
    C.PI_IMPORT_NAME: "TICKETS",
    C.PI_TYPE: "EXP",
    C.PI_PROTOCOLS: [],
    C.PI_DEPENDENCIES: ["XEP-0060", "PUBSUB_SCHEMA", "XEP-0277", "IDENTITY"],
    C.PI_MAIN: "Tickets",
    C.PI_HANDLER: "no",
    C.PI_DESCRIPTION: _("""Tickets management plugin""")
}


class Tickets(object):

    def __init__(self, host):
        log.info(_(u"Tickets plugin initialization"))
        self.host = host
        host.registerNamespace('tickets', NS_TICKETS)
        self._p = self.host.plugins["XEP-0060"]
        self._s = self.host.plugins["PUBSUB_SCHEMA"]
        self._m = self.host.plugins["XEP-0277"]
        self._i = self.host.plugins["IDENTITY"]
        host.bridge.addMethod("ticketsGet", ".plugin",
                              in_sign='ssiassa{ss}s', out_sign='(asa{ss})',
                              method=self._get,
                              async=True
                              )
        host.bridge.addMethod("ticketSet", ".plugin",
                              in_sign='ssa{sas}ssa{ss}s', out_sign='s',
                              method=self._set,
                              async=True)
        host.bridge.addMethod("ticketsSchemaGet", ".plugin",
                              in_sign='sss', out_sign='s',
                              method=self._getSchema,
                              async=True)

    def _reporterFilter(self, client, form_xmlui, widget_type, args, kwargs):
        if not args[0]:
            # if reporter is not filled, we use user part of publisher
            # (if we have it)
            try:
                publisher = jid.JID(form_xmlui.named_widgets['publisher'].value)
            except (KeyError, RuntimeError):
                pass
            else:
                args[0] = publisher.user.capitalize()
        return widget_type, args, kwargs

    def _labelsFilter(self, form_xmlui, widget_type, args, kwargs):
        if widget_type != u'textbox':
            return widget_type, args, kwargs
        widget_type = u'list'
        options = [o for o in args.pop(0).split(u'\n') if o]
        kwargs = {'options': options,
                  'name': kwargs.get('name'),
                  'styles': (u'noselect', u'extensible', u'reducible')}
        return widget_type, args, kwargs

    def _dateFilter(self, form_xmlui, widget_type, args, kwargs):
        if widget_type != u'string' or not args[0]:
            return widget_type, args, kwargs
        # we convert XMPP date to timestamp
        try:
            args[0] = unicode(utils.date_parse(args[0]))
        except Exception as e:
            log.warning(_(u"Can't parse date field: {msg}").format(msg=e))
        return widget_type, args, kwargs

    def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, profile_key=C.PROF_KEY_NONE):
        client = self.host.getClient(profile_key)
        service = jid.JID(service) if service else None
        max_items = None if max_items == C.NO_LIMIT else max_items
        extra = self._p.parseExtra(extra_dict)
        d = self.get(client, service, node or None, max_items, item_ids, sub_id or None, extra.rsm_request, extra.extra)
        d.addCallback(self._p.serItemsData)
        return d

    @defer.inlineCallbacks
    def get(self, client, service=None, node=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, form_ns=NS_TICKETS):
        """Retrieve tickets and convert them to XMLUI

        @param service(None, jid.JID): Pubsub service to use
        @param node(unicode, None): PubSub node to use
            if None, default ticket node will be used
        other parameters as the same as for [XEP_0060.getItems]
        @return (tuple(list[unicode], dict[unicode, object])):
            - XMLUI of the tickets
            - metadata dict
        """
        if not node:
            node = NS_TICKETS
        filters = {u'reporter': lambda *args: self._reporterFilter(client, *args),
                   u'labels': self._labelsFilter,
                   u'created': self._dateFilter,
                   u'updated': self._dateFilter,
                   }
        tickets, metadata = yield self._s.getDataFormItems(
            client,
            form_ns,
            service,
            node,
            max_items = max_items,
            item_ids = item_ids,
            sub_id = sub_id,
            rsm_request = rsm_request,
            extra = extra,
            filters = filters,
            )

        defer.returnValue((tickets, metadata))

    def _set(self, service, node, values, schema=None, item_id=None, extra=None, profile_key=C.PROF_KEY_NONE):
        client = self.host.getClient(profile_key)
        service = None if not service else jid.JID(service)
        if schema:
            schema = generic.parseXml(schema.encode('utf-8'))
        else:
            schema = None
        if extra and u'update' in extra:
            extra[u'update'] = C.bool(extra[u'update'])
        d = self.set(client, service, node or None, values, schema, item_id or None, extra, deserialise=True)
        d.addCallback(lambda ret: ret or u'')
        return d

    @defer.inlineCallbacks
    def set(self, client, service, node, values, schema=None, item_id=None, extra=None, deserialise=False, form_ns=NS_TICKETS):
        """Publish a tickets

        @param node(unicode, None): Pubsub node to use
            None to use default tickets node
        @param values(dict[key(unicode), [iterable[object], object]]): values of the ticket
            if not iterable, will be put in a list
            'created' and 'updated' will be forced to current time:
                - 'created' is set if item_id is None, i.e. if it's a new ticket
                - 'updated' is set everytime
        @param extra(dict, None): same as for [XEP-0060.sendItem] with additional keys:
            - update(bool): if True, get previous item data to merge with current one
                if True, item_id must be None
        other arguments are same as for [self._s.sendDataFormItem]
        @return (unicode): id of the created item
        """
        if not node:
            node = NS_TICKETS
        now = utils.xmpp_date()
        if not item_id:
            values['created'] = now
            comments_service = yield self._m.getCommentsService(client, service)

            # we need to use uuid for comments node, because we don't know item id in advance
            # (we don't want to set it ourselves to let the server choose, so we can have
            #  a nicer id if serial ids is activated)
            comments_node = self._m.getCommentsNode(node + u'_' + unicode(shortuuid.uuid()))
            options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN,
                       self._p.OPT_PERSIST_ITEMS: 1,
                       self._p.OPT_MAX_ITEMS: -1,
                       self._p.OPT_DELIVER_PAYLOADS: 1,
                       self._p.OPT_SEND_ITEM_SUBSCRIBE: 1,
                       self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN,
                       }
            yield self._p.createNode(client, comments_service, comments_node, options)
            values['comments_uri'] = uri.buildXMPPUri(u'pubsub', subtype='microblog', path=comments_service.full(), node=comments_node)
        elif extra.get(u'update', False):
            if item_id is None:
                raise exceptions.DataError(_(u'if extra["update"] is set, item_id must be set too'))
            try:
                # we get previous item
                items_data = yield self._p.getItems(client, service, node, item_ids=[item_id])
                item_elt = items_data[0][0]
            except Exception as e:
                log.warning(_(u"Can't get previous item, update ignored: {reason}").format(
                    reason = e))
            else:
                # and parse it
                form = data_form.findForm(item_elt, form_ns)
                if form is None:
                    log.warning(_(u"Can't parse previous item, update ignored: data form not found").format(
                        reason = e))
                else:
                    for name, field in form.fields.iteritems():
                        if name not in values:
                            values[name] = u'\n'.join(unicode(v) for v in field.values)

        values['updated'] = now
        if not values.get('reporter'):
            identity = yield self._i.getIdentity(client, client.jid)
            values['reporter'] = identity['nick']
        if not values.get('reporter_jid'):
            values['reporter_jid'] = client.jid.full()
        item_id = yield self._s.sendDataFormItem(client, service, node, values, schema, item_id, extra, deserialise)
        defer.returnValue(item_id)

    def _getSchema(self, service, node, profile_key=C.PROF_KEY_NONE):
        if not node:
            node = NS_TICKETS
        return self._s._getUISchema(service, node, profile_key)