# HG changeset patch # User Florent Aide, <florent.aide@gmail.com> # Date 1313666907 -7200 # Thu Aug 18 13:28:27 2011 +0200 # Node ID 9379d7a911df195e019dff39530b65aaa47ca09c # Parent 80d3c51f0639807101f182478ffe82e7f1974572 We now have tracking in place in the OpenERP part diff --git a/__init__.py b/__init__.py --- a/__init__.py +++ b/__init__.py @@ -2,6 +2,7 @@ ############################################################################## # # Email marketing click & read tracker +# Copyright (C) 2011 Florent Aide, <florent.aide@gmail.com> # Copyright (C) 2011 XCG Consulting (http://www.xcg-consulting.fr/) # # This program is free software: you can redistribute it and/or modify @@ -19,6 +20,7 @@ # ############################################################################## +import trackitem import marketing_campaign_tracker # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/__openerp__.py b/__openerp__.py --- a/__openerp__.py +++ b/__openerp__.py @@ -2,6 +2,7 @@ ############################################################################## # # Email marketing click & read tracker +# Copyright (C) 2011 Florent Aide, <florent.aide@gmail.com> # Copyright (C) 2011 XCG Consulting (http://www.xcg-consulting.fr/) # # This program is free software: you can redistribute it and/or modify diff --git a/marketing_campaign_tracker.py b/marketing_campaign_tracker.py --- a/marketing_campaign_tracker.py +++ b/marketing_campaign_tracker.py @@ -2,6 +2,7 @@ ############################################################################## # # Email marketing click & read tracker +# Copyright (C) 2011 Florent Aide, <florent.aide@gmail.com> # Copyright (C) 2011 XCG Consulting (http://www.xcg-consulting.fr/) # # This program is free software: you can redistribute it and/or modify @@ -20,6 +21,92 @@ ############################################################################## from osv import osv, fields +import re +import uuid + +simple_text_url_re = re.compile(r'http://[-a-zA-Z0-9_/.]+') + +def insert_tracker_in_text(cursor, user, text, tracker_base, pool, activity_id): + + def repl(match_obj): + # here we must keep a reference on the uuid in our db for the tracker + real_url = match_obj.group(0) + uid = str(uuid.uuid4()) + values = dict( + trackitem_uuid = uid, + real_url = real_url, + campaign_activity_id = activity_id, + ) + trackitem = pool.get('marketing_campaign_tracker.trackitem') + # create a trackitem for each URL that is replaced + trackitem.create(cursor, user, values, context=None) + return "%s/%s" % (tracker_base, uid) + + return simple_text_url_re.sub(repl, text) + + +def insert_tracker_in_html(cr, uid, html, tracker_base, pool, activity_id): + return html + +class hooked_email_template(osv.osv): + """overrides the base email template to hook the URL tracker + """ + _inherit = 'email.template' + + def generate_mail(self, cursor, user, template_id, + record_ids, context=None): + """since we cannot override the _generate_mailbox_item_from_template + method we hook into the higher level generate mail and are forced to + copy over the code in order to add our own... This is some KIND of + inheritance :) + """ + + if context is None: + context = {} + + template = self.browse(cursor, user, template_id, context=context) + + if not template: + raise Exception("The requested template could not be loaded") + + # CHANGE FROM ORIGINAL METHOD + if context.get('use_tracker', False): + # here we need to use tracker so let's do it + tracker_base = context.get('tracker_base') + text = template.def_body_text + template.def_body_text = insert_tracker_in_text( + cursor, user, text, tracker_base, + self.pool, context['activity_id']) + + html = template.def_body_html + template.def_body_html = insert_tracker_in_html( + cursor, user, html, tracker_base, + self.pool, context['activity_id']) + + # END OF CHANGES FROM ORIGINAL METHOD + + result = True + mailbox_obj = self.pool.get('email_template.mailbox') + for record_id in record_ids: + mailbox_id = self._generate_mailbox_item_from_template( + cursor, user, template, record_id, context) + + mail = mailbox_obj.browse(cursor, user, mailbox_id, context=context) + + if template.report_template or template.attachment_ids: + self.generate_attach_reports(cursor, user, template, + record_id, mail, context) + + self.pool.get('email_template.mailbox').write( + cursor, user, mailbox_id, {'folder':'outbox'}, context=context) + + # TODO : manage return value of all the records + result = self.pool.get('email_template.mailbox').send_this_mail( + cursor, user, [mailbox_id], context) + + return result + +hooked_email_template() class marketing_campaign_activity(osv.osv): @@ -43,9 +130,16 @@ # override the _process_wi_email to replace all the URLs in the template # body by our tracker URL and store the corresponding keys in database def _process_wi_email(self, cr, uid, activity, workitem, context=None): - print "*"*35 - print "Processing workitem for email!" - print "*"*35 + if context is None: + context = {} + + context['use_tracker'] = activity.use_tracker + + if activity.use_tracker: + # TODO: get it from the tracker reference on the activity and remove trailing / if any + context['tracker_base'] = 'http://localhost:9965' + context['activity_id'] = activity.id + return self.pool.get('email.template').generate_mail(cr, uid, activity.email_template_id.id, [workitem.res_id], context=context) diff --git a/trackitem.py b/trackitem.py new file mode 100644 --- /dev/null +++ b/trackitem.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Email marketing click & read tracker +# Copyright (C) 2011 Florent Aide, <florent.aide@gmail.com> +# Copyright (C) 2011 XCG Consulting (http://www.xcg-consulting.fr/) +# +# 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 osv import osv, fields +import time + +class trackitem(osv.osv): + """a trackitem is a simple link between a real URL and a UUID URL that + was sent in an email. When the person clicks on the UUID URL the tracker + server will hit the database with the UUID to try and find the real URL + and then redirect the visitor to the real URL he wants to see. + During this process the tracker will be responsible to create trackvisit + objects associated with the relevant trackitem. + + A trackitem is also directly linked to a campaign activity in order to be + easily able to analyze by campaign the click/read ratios + """ + _name = 'marketing_campaign_tracker.trackitem' + + _columns = { + 'trackitem_uuid': fields.char("uuid", size=36, required=True, select=1), + 'real_url': fields.char("Real URL", size=500), + # we only need the activity because an activity obj has a campaign_id + # attribute referencing the campaign it belongs to. + 'campaign_activity_id': fields.many2one( + 'marketing.campaign.activity', + 'Marketing Campaign Activity', + ), + } + +trackitem() + +class trackvisit(osv.osv): + """a trackvisit is a small timestamp associated to a trackitem + Basically each time a client clicks on a tracked URL and gets redirected + by the tracker a trackvisit will be recorded for the action. This opens-up + interesting trails of who clicks/views our marketing emails and how many + times, when, from where... + """ + + _name = 'marketing_campaign_tracker.trackvisit' + + _columns = { + # keep a reference to our parent + 'trackitem_id': fields.many2one( + 'marketing_campaign_tracker.trackitem', + 'Track Item', + ), + 'visit_time': fields.datetime('Visit time', required=True), + } + + _defaults = dict( + # format the datetime for PG backend storage in a timestamp + visit_time = lambda *a: time.strftime("%Y-%m-%d %H:%M:%S.000000"), + ) + +trackvisit()