diff --git a/modules/marketing_campaign/CHANGELOG b/modules/marketing_campaign/CHANGELOG index e8c835234b..ca61be95de 100644 --- a/modules/marketing_campaign/CHANGELOG +++ b/modules/marketing_campaign/CHANGELOG @@ -1,3 +1,4 @@ +* Add UTM parameters to shortened URL Version 7.0.0 - 2023-10-30 -------------------------- diff --git a/modules/marketing_campaign/__init__.py b/modules/marketing_campaign/__init__.py index 43c550da9f..e1091f3f81 100644 --- a/modules/marketing_campaign/__init__.py +++ b/modules/marketing_campaign/__init__.py @@ -13,10 +13,11 @@ try: except ImportError: OpportunityAbstract = None -from . import marketing, sale, sale_opportunity_reporting, sale_reporting -from .marketing import MarketingCampaignMixin, Parameter +from . import marketing, sale, sale_opportunity_reporting, sale_reporting, web +from .marketing import MarketingCampaignMixin, MarketingCampaignUTM, Parameter -__all__ = ['register', 'Parameter', 'MarketingCampaignMixin'] +__all__ = [ + 'register', 'Parameter', 'MarketingCampaignMixin', 'MarketingCampaignUTM'] def register(): @@ -41,6 +42,9 @@ def register(): Pool.register( sale.POSSale, module='marketing_campaign', type_='model', depends=['sale_point']) + Pool.register( + web.ShortenedURL, + module='marketing_campaign', type_='model', depends=['web_shortener']) if SaleAbstract: Pool.register_mixin( sale_reporting.AbstractMarketingCampaign, SaleAbstract, diff --git a/modules/marketing_campaign/doc/reference.rst b/modules/marketing_campaign/doc/reference.rst index 354edb044f..be30f463c7 100644 --- a/modules/marketing_campaign/doc/reference.rst +++ b/modules/marketing_campaign/doc/reference.rst @@ -31,4 +31,10 @@ Campaign Yield the field names of the :class:`Parameter` fields. +.. class:: MarketingCampaignUTM + + The *MarketingCampaignUTM* is a mixin_ that helps to add UTM_ parameters to + URL. + .. _mixin: https://en.wikipedia.org/wiki/Mixin +.. _UTM: https://en.wikipedia.org/wiki/UTM_parameters diff --git a/modules/marketing_campaign/marketing.py b/modules/marketing_campaign/marketing.py index f4aa28d6bd..c68fd81812 100644 --- a/modules/marketing_campaign/marketing.py +++ b/modules/marketing_campaign/marketing.py @@ -1,6 +1,8 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse + from sql.functions import Lower from sql.operators import Equal @@ -78,6 +80,38 @@ class Source(Parameter): __name__ = 'marketing.source' +class MarketingCampaignUTM: + __slots__ = () + + @property + def utm_campaign(self): + if campaign := getattr(self, 'marketing_campaign', None): + return campaign.name + + @property + def utm_medium(self): + if medium := getattr(self, 'marketing_medium', None): + return medium.name + + @property + def utm_source(self): + if source := getattr(self, 'marketing_source', None): + return source.name + + def add_utm_parameters(self, url): + params = {} + for name in ['utm_campaign', 'utm_medium', 'utm_source']: + if value := getattr(self, name): + params[name] = value + if params: + url_parts = list(urlparse(url)) + query = dict(parse_qsl(url_parts[4])) + query.update(params) + url_parts[4] = urlencode(query) + url = urlunparse(url_parts) + return url + + class MarketingCampaignMixin(Model): marketing_campaign = fields.Many2One( diff --git a/modules/marketing_campaign/setup.py b/modules/marketing_campaign/setup.py index 76f3a810e1..0c657bae69 100755 --- a/modules/marketing_campaign/setup.py +++ b/modules/marketing_campaign/setup.py @@ -55,6 +55,7 @@ tests_require = [ get_require_version('trytond_sale'), get_require_version('trytond_sale_opportunity'), get_require_version('trytond_sale_point'), + get_require_version('trytond_web_shortener'), ] setup(name=name, diff --git a/modules/marketing_campaign/tests/test_module.py b/modules/marketing_campaign/tests/test_module.py index 712f361d16..1ac33fdcda 100644 --- a/modules/marketing_campaign/tests/test_module.py +++ b/modules/marketing_campaign/tests/test_module.py @@ -1,12 +1,47 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from trytond.tests.test_tryton import ModuleTestCase + +from unittest.mock import patch + +from trytond.model import ModelStorage +from trytond.modules.marketing_campaign import MarketingCampaignUTM +from trytond.pool import Pool +from trytond.tests.test_tryton import ModuleTestCase, with_transaction class MarketingCampaignTestCase(ModuleTestCase): "Test Marketing Campaign module" module = 'marketing_campaign' - extras = ['sale', 'sale_opportunity', 'sale_point'] + extras = ['sale', 'sale_opportunity', 'sale_point', 'web_shortener'] + + @with_transaction() + def test_shortened_url_add_utm(self): + "Test add UTM to shortened URL" + pool = Pool() + ShortenedURL = pool.get('web.shortened_url') + URLAccess = pool.get('web.shortened_url.access') + Campaign = pool.get('marketing.campaign') + + with patch.object(URLAccess, 'save'): + class Record(MarketingCampaignUTM, ModelStorage): + __slots__ = ('marketing_campaign',) + id = 1 + + Record.__setup__() + Record.__post_setup__() + + campaign = Campaign(name='campaign') + campaign.save() + record = Record() + record.marketing_campaign = campaign + shortened_url = ShortenedURL( + redirect_url='http://example.com/', + record=record, + method=None) + + self.assertEqual( + shortened_url.access(), + 'http://example.com/?utm_campaign=campaign') del ModuleTestCase diff --git a/modules/marketing_campaign/tryton.cfg b/modules/marketing_campaign/tryton.cfg index 95c1abaef7..c29f49a9c0 100644 --- a/modules/marketing_campaign/tryton.cfg +++ b/modules/marketing_campaign/tryton.cfg @@ -7,6 +7,7 @@ extras_depend: sale sale_opportunity sale_point + web_shortener xml: marketing.xml sale.xml diff --git a/modules/marketing_campaign/web.py b/modules/marketing_campaign/web.py new file mode 100644 index 0000000000..d47401da16 --- /dev/null +++ b/modules/marketing_campaign/web.py @@ -0,0 +1,17 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + + +from trytond.pool import PoolMeta + +from .marketing import MarketingCampaignUTM + + +class ShortenedURL(metaclass=PoolMeta): + __name__ = 'web.shortened_url' + + def access(self, **values): + url = super().access(**values) + if isinstance(self.record, MarketingCampaignUTM): + url = self.record.add_utm_parameters(url) + return url