1# -*- coding: utf-8 -*-
2
3import re
4import werkzeug
5
6from odoo import tools
7from odoo.addons.mass_mailing.tests.common import MassMailCommon
8from odoo.addons.sms.tests.common import SMSCase, SMSCommon
9
10
11class MassSMSCase(SMSCase):
12
13    # ------------------------------------------------------------
14    # ASSERTS
15    # ------------------------------------------------------------
16
17    def assertSMSStatistics(self, recipients_info, mailing, records, check_sms=True):
18        """ Deprecated, remove in 14.4 """
19        return self.assertSMSTraces(recipients_info, mailing, records, check_sms=check_sms)
20
21    def assertSMSTraces(self, recipients_info, mailing, records,
22                        check_sms=True, sent_unlink=False,
23                        sms_links_info=None):
24        """ Check content of traces. Traces are fetched based on a given mailing
25        and records. Their content is compared to recipients_info structure that
26        holds expected information. Links content may be checked, notably to
27        assert shortening or unsubscribe links. Sms.sms records may optionally
28        be checked.
29
30        :param recipients_info: list[{
31          # TRACE
32          'partner': res.partner record (may be empty),
33          'number': number used for notification (may be empty, computed based on partner),
34          'state': outgoing / sent / ignored / bounced / exception / opened (outgoing by default),
35          'record: linked record,
36          # SMS.SMS
37          'content': optional: if set, check content of sent SMS;
38          'failure_type': error code linked to sms failure (see ``error_code``
39            field on ``sms.sms`` model);
40          },
41          { ... }];
42        :param mailing: a mailing.mailing record from which traces have been
43          generated;
44        :param records: records given to mailing that generated traces. It is
45          used notably to find traces using their IDs;
46        :param check_sms: if set, check sms.sms records that should be linked to traces;
47        :param sent_unlink: it True, sent sms.sms are deleted and we check gateway
48          output result instead of actual sms.sms records;
49        :param sms_links_info: if given, should follow order of ``recipients_info``
50          and give details about links. See ``assertLinkShortenedHtml`` helper for
51          more details about content to give;
52        ]
53        """
54        # map trace state to sms state
55        state_mapping = {
56            'sent': 'sent',
57            'outgoing': 'outgoing',
58            'exception': 'error',
59            'ignored': 'canceled',
60            'bounced': 'error',
61        }
62        traces = self.env['mailing.trace'].search([
63            ('mass_mailing_id', 'in', mailing.ids),
64            ('res_id', 'in', records.ids)
65        ])
66
67        self.assertTrue(all(s.model == records._name for s in traces))
68        # self.assertTrue(all(s.utm_campaign_id == mailing.campaign_id for s in traces))
69        self.assertEqual(set(s.res_id for s in traces), set(records.ids))
70
71        # check each trace
72        if not sms_links_info:
73            sms_links_info = [None] * len(recipients_info)
74        for recipient_info, link_info, record in zip(recipients_info, sms_links_info, records):
75            partner = recipient_info.get('partner', self.env['res.partner'])
76            number = recipient_info.get('number')
77            state = recipient_info.get('state', 'outgoing')
78            content = recipient_info.get('content', None)
79            if number is None and partner:
80                number = partner._sms_get_recipients_info()[partner.id]['sanitized']
81
82            trace = traces.filtered(
83                lambda t: t.sms_number == number and t.state == state and (t.res_id == record.id if record else True)
84            )
85            self.assertTrue(len(trace) == 1,
86                            'SMS: found %s notification for number %s, (state: %s) (1 expected)' % (len(trace), number, state))
87            self.assertTrue(bool(trace.sms_sms_id_int))
88
89            if check_sms:
90                if state == 'sent':
91                    if sent_unlink:
92                        self.assertSMSIapSent([number], content=content)
93                    else:
94                        self.assertSMS(partner, number, 'sent', content=content)
95                elif state in state_mapping:
96                    sms_state = state_mapping[state]
97                    error_code = recipient_info['failure_type'] if state in ('exception', 'ignored', 'bounced') else None
98                    self.assertSMS(partner, number, sms_state, error_code=error_code, content=content)
99                else:
100                    raise NotImplementedError()
101
102            if link_info:
103                # shortened links are directly included in sms.sms record as well as
104                # in sent sms (not like mails who are post-processed)
105                sms_sent = self._find_sms_sent(partner, number)
106                sms_sms = self._find_sms_sms(partner, number, state_mapping[state])
107                for (url, is_shortened, add_link_params) in link_info:
108                    if url == 'unsubscribe':
109                        url = '%s/sms/%d/%s' % (mailing.get_base_url(), mailing.id, trace.sms_code)
110                    link_params = {'utm_medium': 'SMS', 'utm_source': mailing.name}
111                    if add_link_params:
112                        link_params.update(**add_link_params)
113                    self.assertLinkShortenedText(
114                        sms_sms.body,
115                        (url, is_shortened),
116                        link_params=link_params,
117                    )
118                    self.assertLinkShortenedText(
119                        sms_sent['body'],
120                        (url, is_shortened),
121                        link_params=link_params,
122                    )
123
124    # ------------------------------------------------------------
125    # GATEWAY TOOLS
126    # ------------------------------------------------------------
127
128    def gateway_sms_click(self, mailing, record):
129        """ Simulate a click on a sent SMS. Usage: giving a partner and/or
130        a number, find an SMS sent to him, find shortened links in its body
131        and call add_click to simulate a click. """
132        trace = mailing.mailing_trace_ids.filtered(lambda t: t.model == record._name and t.res_id == record.id)
133        sms_sent = self._find_sms_sent(self.env['res.partner'], trace.sms_number)
134        self.assertTrue(bool(sms_sent))
135        return self.gateway_sms_sent_click(sms_sent)
136
137    def gateway_sms_sent_click(self, sms_sent):
138        """ When clicking on a link in a SMS we actually don't have any
139        easy information in body, only body. We currently click on all found
140        shortened links. """
141        for url in re.findall(tools.TEXT_URL_REGEX, sms_sent['body']):
142            if '/r/' in url:  # shortened link, like 'http://localhost:8069/r/LBG/s/53'
143                parsed_url = werkzeug.urls.url_parse(url)
144                path_items = parsed_url.path.split('/')
145                code, sms_sms_id = path_items[2], int(path_items[4])
146                trace_id = self.env['mailing.trace'].sudo().search([('sms_sms_id_int', '=', sms_sms_id)]).id
147
148                self.env['link.tracker.click'].sudo().add_click(
149                    code,
150                    ip='100.200.300.400',
151                    country_code='BE',
152                    mailing_trace_id=trace_id
153                )
154
155
156class MassSMSCommon(MassMailCommon, SMSCommon, MassSMSCase):
157
158    @classmethod
159    def setUpClass(cls):
160        super(MassSMSCommon, cls).setUpClass()
161