Mercurial > libervia-backend
changeset 2471:544c4d2fec45
plugins schema, merge_requests, tickets*: factorisation
Dode common in plugins using schema have been factorised in pubsub schema plugin, and filters users in tickets handling have been renamed in a more generic way and put there too.
"reporter*" fields in tickets have been renamed to "author*" as it is a more generic term which can be used elsewhere.
The use of new utils.partial function make easy the creation of simple plugins using schema.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 12 Jan 2018 15:58:54 +0100 |
parents | 8084066ac95b |
children | 3f0a3a0ed290 |
files | src/plugins/plugin_exp_pubsub_schema.py src/plugins/plugin_misc_merge_requests.py src/plugins/plugin_misc_tickets.py src/plugins/plugin_tickets_import.py src/plugins/plugin_tickets_import_bugzilla.py |
diffstat | 5 files changed, 222 insertions(+), 169 deletions(-) [+] |
line wrap: on
line diff
--- a/src/plugins/plugin_exp_pubsub_schema.py Fri Jan 12 15:45:37 2018 +0100 +++ b/src/plugins/plugin_exp_pubsub_schema.py Fri Jan 12 15:58:54 2018 +0100 @@ -21,6 +21,7 @@ from sat.core import exceptions from sat.core.constants import Const as C from sat.tools import xml_tools +from sat.tools import utils from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber.xmlstream import XMPPHandler from twisted.internet import defer @@ -41,7 +42,7 @@ C.PI_IMPORT_NAME: "PUBSUB_SCHEMA", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["XEP-0060"], + C.PI_DEPENDENCIES: ["XEP-0060", "IDENTITY"], C.PI_MAIN: "PubsubSchema", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Handle Pubsub data schemas""") @@ -54,6 +55,7 @@ log.info(_(u"PubSub Schema initialization")) self.host = host self._p = self.host.plugins["XEP-0060"] + self._i = self.host.plugins["IDENTITY"] host.bridge.addMethod("psSchemaGet", ".plugin", in_sign='sss', out_sign='s', method=self._getSchema, @@ -66,7 +68,7 @@ ) host.bridge.addMethod("psSchemaUIGet", ".plugin", in_sign='sss', out_sign='s', - method=self._getUISchema, + method=utils.partial(self._getUISchema, default_node=None), async=True ) host.bridge.addMethod("psItemsFormGet", ".plugin", @@ -163,7 +165,11 @@ xmlui = xml_tools.dataForm2XMLUI(form, '') return xmlui - def _getUISchema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE): + def _getUISchema(self, service, nodeIdentifier, default_node=None, profile_key=C.PROF_KEY_NONE): + if not nodeIdentifier: + if not default_node: + raise ValueError(_(u"nodeIndentifier needs to be set")) + nodeIdentifier = default_node client = self.host.getClient(profile_key) service = None if not service else jid.JID(service) d = self.getUISchema(client, service, nodeIdentifier) @@ -208,24 +214,30 @@ schema = None max_items = None if max_items == C.NO_LIMIT else max_items extra = self._p.parseExtra(extra_dict) - d = self.getDataFormItems(client, form_ns or None, service, node, schema, max_items or None, item_ids, sub_id or None, extra.rsm_request, extra.extra) + d = self.getDataFormItems(client, service, node, schema, max_items or None, item_ids, sub_id or None, extra.rsm_request, extra.extra, form_ns=form_ns or None) d.addCallback(self._p.serItemsData) return d @defer.inlineCallbacks - def getDataFormItems(self, client, form_ns, service, nodeIdentifier, schema=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, filters=None): + def getDataFormItems(self, client, service, nodeIdentifier, schema=None, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None, default_node=None, form_ns=None, filters=None): """Get items known as being data forms, and convert them to XMLUI + @param schema(domish.Element, data_form.Form, None): schema of the node if known + if None, it will be retrieved from node + @param default_node(unicode): node to use if nodeIdentifier is None or empty @param form_ns (unicode, None): namespace of the form None to accept everything, even if form has no namespace - @param schema(domish.Element, data_form.Form, None): schema of the node if known - if None, it will be retrieved from node @param filters(dict, None): same as for xml_tools.dataFormResult2XMLUI other parameters as the same as for [getItems] @return (list[unicode]): XMLUI of the forms if an item is invalid (not corresponding to form_ns or not a data_form) it will be skipped + @raise ValueError: one argument is invalid """ + if not nodeIdentifier: + if not default_node: + raise ValueError(_(u"default_node must be set if nodeIdentifier is not set")) + nodeIdentifier = default_node # we need the initial form to get options of fields when suitable schema_form = yield self.getSchemaForm(client, service, nodeIdentifier, schema, form_type='result', copy_form=False) items_data = yield self._p.getItems(client, service, nodeIdentifier, max_items, item_ids, sub_id, rsm_request, extra) @@ -306,6 +318,170 @@ yield self._p.sendItem(client, service, nodeIdentifier, form.toElement(), item_id, extra) + ## filters ## + # filters useful for data form to XMLUI conversion # + + def valueOrPublisherFilter(self, form_xmlui, widget_type, args, kwargs): + """Replace missing value by publisher's user part""" + if not args[0]: + # value 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 textbox2ListFilter(self, form_xmlui, widget_type, args, kwargs): + """Split lines of a textbox in a list + + main use case is using a textbox for labels + """ + 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): + """Convert a string with a date to a unix timestamp""" + 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 + + ## Helper methods ## + + def prepareBridgeGet(self, service, node, max_items, sub_id, extra_dict, profile_key): + """Parse arguments received from bridge *Get methods and return higher level data + + @return (tuple): (client, service, node, max_items, extra, sub_id) usable for internal methods + """ + client = self.host.getClient(profile_key) + service = jid.JID(service) if service else None + if not node: + node = None + max_items = None if max_items == C.NO_LIMIT else max_items + if not sub_id: + sub_id = None + extra = self._p.parseExtra(extra_dict) + + return client, service, node, max_items, extra, sub_id + + def _get(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, default_node=None, form_ns=None, filters=None, profile_key=C.PROF_KEY_NONE): + """Bridge method to retrieve data from node with schema + + this method is a helper so dependant plugins can use it directly + when adding *Get methods + """ + client, service, node, max_items, extra, sub_id = self.prepareBridgeGet(service, node, max_items, sub_id, extra_dict, profile_key) + d = self.getDataFormItems(client, service, node or None, + max_items=max_items, + item_ids=item_ids, + sub_id=sub_id, + rsm_request=extra.rsm_request, + extra=extra.extra, + default_node=default_node, + form_ns=form_ns, + filters=filters) + d.addCallback(self._p.serItemsData) + return d + + def prepareBridgeSet(self, service, node, schema, item_id, extra, profile_key): + """Parse arguments received from bridge *Set methods and return higher level data + + @return (tuple): (client, service, node, schema, item_id, extra) usable for internal methods + """ + 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']) + return client, service, node or None, schema, item_id or None, extra + + def _set(self, service, node, values, schema=None, item_id=None, extra=None, default_node=None, form_ns=None, fill_author=True, profile_key=C.PROF_KEY_NONE): + """Bridge method to set item in node with schema + + this method is a helper so dependant plugins can use it directly + when adding *Set methods + """ + client, service, node, schema, item_id, extra = self.prepareBridgeSet(service, node, schema, item_id, extra) + d = self.set(client, service, node, values, schema, item_id, extra, + deserialise=True, + form_ns=form_ns, + default_node=default_node, + fill_author=fill_author) + d.addCallback(lambda ret: ret or u'') + return d + + @defer.inlineCallbacks + def set(self, client, service, node, values, schema, item_id, extra, deserialise, form_ns, default_node=None, fill_author=True): + """Set an item in a node with a schema + + This method can be used directly by *Set methods added by dependant plugin + @param values(dict[key(unicode), [iterable[object], object]]): values of the items + 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 + @param form_ns (unicode, None): namespace of the form + needed when an update is done + @param default_node(unicode, None): value to use if node is not set + other arguments are same as for [self._s.sendDataFormItem] + @return (unicode): id of the created item + """ + if not node: + if default_node is None: + raise ValueError(_(u"default_node must be set if node is not set")) + node = default_node + now = utils.xmpp_date() + if not item_id: + values['created'] = now + 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 fill_author: + if not values.get('author'): + identity = yield self._i.getIdentity(client, client.jid) + values['author'] = identity['nick'] + if not values.get('author_jid'): + values['author_jid'] = client.jid.full() + item_id = yield self.sendDataFormItem(client, service, node, values, schema, item_id, extra, deserialise) + defer.returnValue(item_id) + class SchemaHandler(XMPPHandler): implements(iwokkel.IDisco)
--- a/src/plugins/plugin_misc_merge_requests.py Fri Jan 12 15:45:37 2018 +0100 +++ b/src/plugins/plugin_misc_merge_requests.py Fri Jan 12 15:58:54 2018 +0100 @@ -20,10 +20,9 @@ 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 collections import namedtuple +from sat.tools import utils from sat.core.log import getLogger log = getLogger(__name__) @@ -34,7 +33,7 @@ C.PI_IMPORT_NAME: "MERGE_REQUESTS", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["XEP-0060", "PUBSUB_SCHEMA", "TICKETS"], + C.PI_DEPENDENCIES: ["XEP-0060", "PUBSUB_SCHEMA"], C.PI_MAIN: "MergeRequests", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Merge requests management plugin""") @@ -69,7 +68,6 @@ host.registerNamespace('merge_requests', NS_MERGE_REQUESTS) self._p = self.host.plugins["XEP-0060"] self._s = self.host.plugins["PUBSUB_SCHEMA"] - self._t = self.host.plugins["TICKETS"] self._handlers = {} self._handlers_list = [] # handlers sorted by priority self._type_handlers = {} # data type => handler map @@ -84,7 +82,7 @@ async=True) host.bridge.addMethod("mergeRequestsSchemaGet", ".plugin", in_sign='sss', out_sign='s', - method=self._getSchema, + method=utils.partial(self._s._getUISchema, default_node=NS_MERGE_REQUESTS), async=True) host.bridge.addMethod("mergeRequestParseData", ".plugin", in_sign='ss', out_sign='aa{ss}', @@ -127,12 +125,9 @@ self._type_handlers[data_type] = self._handlers[name] 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 if extra_dict and 'parse' in extra_dict: extra_dict['parse'] = C.bool(extra_dict['parse']) - extra = self._p.parseExtra(extra_dict) + client, service, node, max_items, extra, sub_id = self._s.prepareBridgeGet(service, node, max_items, sub_id, extra_dict, profile_key) d = self.get(client, service, node or None, max_items, item_ids, sub_id or None, extra.rsm_request, extra.extra) d.addCallback(lambda (tickets, metadata, parsed_patches): ( self._p.serItemsData((tickets, metadata)) + @@ -153,7 +148,16 @@ """ if not node: node = NS_MERGE_REQUESTS - tickets_xmlui, metadata = yield self._t.get(client, service, node, max_items, item_ids, sub_id, rsm_request, extra, form_ns=NS_MERGE_REQUESTS) + tickets_xmlui, metadata = yield self._s.getDataFormItems( + client, + service, + node, + max_items=max_items, + item_ids=item_ids, + sub_id=sub_id, + rsm_request=rsm_request, + extra=extra, + form_ns=NS_MERGE_REQUESTS) parsed_patches = [] if extra.get('parse', False): for ticket in tickets_xmlui: @@ -164,14 +168,7 @@ defer.returnValue((tickets_xmlui, metadata, parsed_patches)) def _set(self, service, node, repository, method, 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 extra and 'update' in extra: - extra['update'] = C.bool(extra['update']) - if schema: - schema = generic.parseXml(schema.encode('utf-8')) - else: - schema = None + client, service, schema, extra = self._s.prepareBridgeSet(service, node, schema, extra) d = self.set(client, service, node or None, repository, method, values, schema, item_id or None, extra, deserialise=True) d.addCallback(lambda ret: ret or u'') return d @@ -188,9 +185,6 @@ other arguments are same as for [TICKETS.set] @return (unicode): id of the created item """ - if not node: - node = NS_MERGE_REQUESTS - if values is None: values = {} @@ -230,14 +224,9 @@ values[FIELD_DATA] = data - item_id = yield self._t.set(client, service, node, values, schema, item_id, extra, deserialise, form_ns=NS_MERGE_REQUESTS) + item_id = yield self._t.set(client, service, node, values, schema, item_id, extra, deserialise, default_node=NS_MERGE_REQUESTS, form_ns=NS_MERGE_REQUESTS) defer.returnValue(item_id) - def _getSchema(self, service, node, profile_key=C.PROF_KEY_NONE): - if not node: - node = NS_MERGE_REQUESTS - return self._s._getUISchema(service, node, profile_key) - def _parseData(self, data_type, data): d = self.parseData(data_type, data) d.addCallback(lambda parsed_patches:
--- a/src/plugins/plugin_misc_tickets.py Fri Jan 12 15:45:37 2018 +0100 +++ b/src/plugins/plugin_misc_tickets.py Fri Jan 12 15:58:54 2018 +0100 @@ -19,15 +19,11 @@ 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.common import uri from sat.tools import utils -from sat.tools.common import uri +import shortuuid from sat.core.log import getLogger -import shortuuid -from wokkel import data_form log = getLogger(__name__) NS_TICKETS = 'org.salut-a-toi.tickets:0' @@ -53,10 +49,18 @@ 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, + method=utils.partial( + self._s._get, + default_node=NS_TICKETS, + form_ns=NS_TICKETS, + filters = {u'author': self._s.valueOrPublisherFilter, + u'labels': self._s.textbox2ListFilter, + u'created': self._s.dateFilter, + u'updated': self._s.dateFilter, + }), + async=True ) host.bridge.addMethod("ticketSet", ".plugin", @@ -65,93 +69,11 @@ async=True) host.bridge.addMethod("ticketsSchemaGet", ".plugin", in_sign='sss', out_sign='s', - method=self._getSchema, + method=utils.partial(self._s._getUISchema, default_node=NS_TICKETS), 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']) + client, service, schema, extra = self._s.prepareBridgeSet(service, node, schema, item_id, extra) 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 @@ -175,9 +97,7 @@ """ 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 @@ -193,37 +113,5 @@ } 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) + item_id = yield self._s.set(client, service, node, values, schema, item_id, extra, deserialise, form_ns) 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)
--- a/src/plugins/plugin_tickets_import.py Fri Jan 12 15:45:37 2018 +0100 +++ b/src/plugins/plugin_tickets_import.py Fri Jan 12 15:58:54 2018 +0100 @@ -71,9 +71,9 @@ 'body': main description of the ticket 'created': date of creation (unix time) 'updated': date of last update (unix time) - 'reporter': full name of reporter - 'reporter_jid': jid of reporter - 'reporter_email': email of reporter + 'author': full name of reporter + 'author_jid': jid of reporter + 'author_email': email of reporter 'assigned_to_name': full name of person working on it 'assigned_to_email': email of person working on it 'cc_emails': list of emails subscribed to the ticket
--- a/src/plugins/plugin_tickets_import_bugzilla.py Fri Jan 12 15:45:37 2018 +0100 +++ b/src/plugins/plugin_tickets_import_bugzilla.py Fri Jan 12 15:58:54 2018 +0100 @@ -72,13 +72,13 @@ ticket['updated'] = utils.date_parse(bug.findtext('delta_ts')) ticket['title'] = bug.findtext('short_desc') reporter_elt = bug.find('reporter') - ticket['reporter'] = reporter_elt.get('name') - if ticket['reporter'] is None: + ticket['author'] = reporter_elt.get('name') + if ticket['author'] is None: if '@' in reporter_elt.text: - ticket['reporter'] = reporter_elt.text[:reporter_elt.text.find('@')].title() + ticket['author'] = reporter_elt.text[:reporter_elt.text.find('@')].title() else: - ticket['reporter'] = u'no name' - ticket['reporter_email'] = reporter_elt.text + ticket['author'] = u'no name' + ticket['author_email'] = reporter_elt.text assigned_to_elt = bug.find('assigned_to') ticket['assigned_to_name'] = assigned_to_elt.get('name') ticket['assigned_to_email'] = assigned_to_elt.text