diff options
| author | Julien De Coster <jdc@odoo.com> | 2014-08-12 14:54:26 +0530 |
|---|---|---|
| committer | Julien De Coster <jdc@odoo.com> | 2014-12-17 17:14:33 +0100 |
| commit | ddac26cdbb3535239f5dbb41f5b464366065f3ba (patch) | |
| tree | b54e4d34060f1c7f80a8c6404f7f4fd56d914e52 /addons | |
| parent | 7f97bf4ec07329e58c818632bf23017b390bb099 (diff) | |
[ADD] Add the website_links module.
This module tracks clicks in mass mailing mails and allow the generation of trackable links in a website interface.
Modules modifications
---------------------
Refractoring of the crm_tracking_* classes in a new module.
* Extract the crm_tracking_* from the crm module into a new module "utm"
* Remove the crm_mass_mailing bridge module
* New dependencies of mass_mailing and website_links to utm.
Diffstat (limited to 'addons')
73 files changed, 4524 insertions, 1550 deletions
diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 45f84b899ad..86efc8a4607 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -29,84 +29,6 @@ AVAILABLE_PRIORITIES = [ ('3', 'Very High'), ] - -class crm_tracking_medium(osv.Model): - # OLD crm.case.channel - _name = "crm.tracking.medium" - _description = "Channels" - _order = 'name' - _columns = { - 'name': fields.char('Channel Name', required=True), - 'active': fields.boolean('Active'), - } - _defaults = { - 'active': lambda *a: 1, - } - - -class crm_tracking_campaign(osv.Model): - # OLD crm.case.resource.type - _name = "crm.tracking.campaign" - _description = "Campaign" - _rec_name = "name" - _columns = { - 'name': fields.char('Campaign Name', required=True, translate=True), - 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'), - } - - -class crm_tracking_source(osv.Model): - _name = "crm.tracking.source" - _description = "Source" - _rec_name = "name" - _columns = { - 'name': fields.char('Source Name', required=True, translate=True), - } - - -class crm_tracking_mixin(osv.AbstractModel): - """Mixin class for objects which can be tracked by marketing. """ - _name = 'crm.tracking.mixin' - - _columns = { - 'campaign_id': fields.many2one('crm.tracking.campaign', 'Campaign', # old domain ="['|',('team_id','=',team_id),('team_id','=',False)]" - help="This is a name that helps you keep track of your different campaign efforts Ex: Fall_Drive, Christmas_Special"), - 'source_id': fields.many2one('crm.tracking.source', 'Source', help="This is the source of the link Ex: Search Engine, another domain, or name of email list"), - 'medium_id': fields.many2one('crm.tracking.medium', 'Channel', help="This is the method of delivery. Ex: Postcard, Email, or Banner Ad", oldname='channel_id'), - } - - def tracking_fields(self): - return [('utm_campaign', 'campaign_id'), ('utm_source', 'source_id'), ('utm_medium', 'medium_id')] - - def tracking_get_values(self, cr, uid, vals, context=None): - for key, fname in self.tracking_fields(): - field = self._fields[fname] - value = vals.get(fname) or (request and request.httprequest.cookies.get(key)) # params.get should be always in session by the dispatch from ir_http - if field.type == 'many2one' and isinstance(value, basestring): - # if we receive a string for a many2one, we search/create the id - if value: - Model = self.pool[field.comodel_name] - rel_id = Model.name_search(cr, uid, value, context=context) - if rel_id: - rel_id = rel_id[0][0] - else: - rel_id = Model.create(cr, uid, {'name': value}, context=context) - vals[fname] = rel_id - else: - # Here the code for others cases that many2one - vals[fname] = value - return vals - - def _get_default_track(self, cr, uid, field, context=None): - return self.tracking_get_values(cr, uid, {}, context=context).get(field) - - _defaults = { - 'source_id': lambda self, cr, uid, ctx: self._get_default_track(cr, uid, 'source_id', ctx), - 'campaign_id': lambda self, cr, uid, ctx: self._get_default_track(cr, uid, 'campaign_id', ctx), - 'medium_id': lambda self, cr, uid, ctx: self._get_default_track(cr, uid, 'medium_id', ctx), - } - - class crm_stage(osv.Model): """ Model for case stages. This models the main stages of a document management flow. Main CRM objects (leads, opportunities, project diff --git a/addons/crm/crm_data.xml b/addons/crm/crm_data.xml index 98abcbf6e23..6d658f3f972 100644 --- a/addons/crm/crm_data.xml +++ b/addons/crm/crm_data.xml @@ -13,45 +13,6 @@ <field name="state">open</field> </record> - - <!-- Crm Source --> - <record model="crm.tracking.source" id="crm_source_search_engine"> - <field name="name">Search engine</field> - </record> - <record model="crm.tracking.source" id="crm_source_mailing"> - <field name="name">Mailing Partner</field> - </record> - <record model="crm.tracking.source" id="crm_source_newsletter"> - <field name="name">Newsletter</field> - </record> - - <!-- Crm Medium --> - <record model="crm.tracking.medium" id="crm_medium_website"> - <field name="name">Website</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_phone"> - <field name="name">Phone</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_direct"> - <field name="name">Direct</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_email"> - <field name="name">Email</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_banner"> - <field name="name">Banner Ad</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_twitter"> - <field name="name">Twitter Ads</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_television"> - <field name="name">Television</field> - </record> - <record model="crm.tracking.medium" id="crm_medium_google_adwords"> - <field name="name">Google Adwords</field> - </record> - - <record model="crm.team" id="sales_team.team_sales_department"> <field name="use_leads">True</field> <field name="alias_name">sales</field> diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index e259de4cb44..6c51afa6a41 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -70,7 +70,7 @@ class crm_lead(format_address, osv.osv): _name = "crm.lead" _description = "Lead/Opportunity" _order = "priority desc,date_action,id desc" - _inherit = ['mail.thread', 'ir.needaction_mixin', 'crm.tracking.mixin'] + _inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin'] _track = { 'stage_id': { diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml index e10f80a4fbb..b8ee10ac12d 100644 --- a/addons/crm/crm_lead_data.xml +++ b/addons/crm/crm_lead_data.xml @@ -67,24 +67,7 @@ (4, ref('stage_lead5')), (4, ref('stage_lead6')), (4, ref('stage_lead7'))]"/> </record> - - <!-- Crm Campain --> - <record model="crm.tracking.campaign" id="crm_campaign_fall_drive"> - <field name="name">Fall Drive</field> - <field name="team_id" ref="sales_team.team_sales_department"/> - </record> - <record model="crm.tracking.campaign" id="crm_campaign_christmas_special"> - <field name="name">Christmas Special</field> - <field name="team_id" ref="sales_team.team_sales_department"/> - </record> - <record model="crm.tracking.campaign" id="crm_campaign_email_campaign_services"> - <field name="name">Email Campaign - Services</field> - <field name="team_id" ref="sales_team.team_sales_department"/> - </record> - <record model="crm.tracking.campaign" id="crm_campaign_email_campaign_products"> - <field name="name">Email Campaign - Products</field> - <field name="team_id" ref="sales_team.team_sales_department"/> - </record> + <!-- crm categories --> <record model="crm.lead.tag" id="categ_oppor1"> <field name="name">Product</field> diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml index a5865b9d3be..92d80d3949b 100644 --- a/addons/crm/crm_lead_demo.xml +++ b/addons/crm/crm_lead_demo.xml @@ -25,9 +25,9 @@ I am Jacques from Le Club SARL. I am interested to attend a training organized in your company. Could you please send me the details ?</field> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_email"/> - <field name="source_id" ref="crm_source_mailing"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_mailing"/> </record> <record id="crm_case_1" model="crm.lead"> <field name="create_date" eval="(DateTime.today() - relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/> @@ -57,9 +57,9 @@ Could you please send me the details ?</field> <field name="team_id" ref="sales_team.team_sales_department"/> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_christmas_special"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_christmas_special"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_2" model="crm.lead"> <field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/> @@ -87,9 +87,9 @@ Could you please send me the details ?</field> <field name="team_id" ref="sales_team.crm_team_1"/> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_google_adwords"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_google_adwords"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_2" model="crm.lead"> <field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/> @@ -109,9 +109,9 @@ Could you please send me the details ?</field> <field name="team_id" ref="sales_team.crm_team_2"/> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="stage_lead6"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_banner"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_banner"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_2" model="crm.lead"> <field name="create_date" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/> @@ -143,9 +143,9 @@ Purchase Manager Stonage IT, Franklinville Contact: +1 813 494 5005</field> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_6" model="crm.lead"> @@ -163,9 +163,9 @@ Contact: +1 813 494 5005</field> <field name="team_id" ref="sales_team.crm_team_2"/> <field name="user_id" ref=""/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_email"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_7" model="crm.lead"> @@ -183,9 +183,9 @@ Contact: +1 813 494 5005</field> <field name="team_id" ref="sales_team.crm_team_2"/> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_email"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_8" model="crm.lead"> @@ -203,9 +203,9 @@ Contact: +1 813 494 5005</field> <field name="team_id" ref="sales_team.team_sales_department"/> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_google_adwords"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_google_adwords"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_9" model="crm.lead"> @@ -223,9 +223,9 @@ Contact: +1 813 494 5005</field> <field name="team_id" ref="sales_team.team_sales_department"/> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_email"/> - <field name="source_id" ref="crm_source_mailing"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_mailing"/> </record> <record id="crm_case_10" model="crm.lead"> @@ -247,9 +247,9 @@ I would like to know more about specification and cost of laptops of your compan Thanks, Andrew</field> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_11" model="crm.lead"> @@ -266,9 +266,9 @@ Andrew</field> <field name="team_id" ref="sales_team.crm_team_1"/> <field name="user_id" ref=""/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_direct"/> - <field name="source_id" ref="crm_source_mailing"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_direct"/> + <field name="source_id" ref="utm.utm_source_mailing"/> </record> <record id="crm_case_12" model="crm.lead"> @@ -285,9 +285,9 @@ Andrew</field> <field name="team_id" ref="sales_team.team_sales_department"/> <field name="user_id" ref=""/> <field name="stage_id" ref="stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <!-- Call Function to Cancel the leads (set as Dead) --> @@ -324,9 +324,9 @@ Andrew</field> <field name="team_id" ref="sales_team.team_sales_department"/> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="crm.stage_lead1"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_email"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_14" model="crm.lead"> @@ -350,9 +350,9 @@ Andrew</field> <field name="team_id" ref="sales_team.team_sales_department"/> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead3"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_15" model="crm.lead"> @@ -374,8 +374,8 @@ Andrew</field> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead3"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(months=2)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_15" model="crm.lead"> <field name="message_follower_ids" eval="[(3, ref('base.partner_root')), (4, ref('base.partner_demo'))]"/> @@ -402,8 +402,8 @@ Andrew</field> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead3"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="source_id" ref="crm_source_mailing"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="source_id" ref="utm.utm_source_mailing"/> </record> <record id="crm_case_17" model="crm.lead"> @@ -427,9 +427,9 @@ Andrew</field> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead4"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_17" model="crm.lead"> <field name="message_follower_ids" eval="[(3, ref('base.partner_root')), (4, ref('base.partner_demo'))]"/> @@ -456,8 +456,8 @@ Andrew</field> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead4"/> <field eval="1" name="active"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_18" model="crm.lead"> <field name="message_follower_ids" eval="[(4, ref('base.partner_demo'))]"/> @@ -484,8 +484,8 @@ Andrew</field> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="crm.stage_lead4"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(months=3)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="source_id" ref="crm_source_mailing"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="source_id" ref="utm.utm_source_mailing"/> </record> <record id="crm_case_20" model="crm.lead"> @@ -501,8 +501,8 @@ Andrew</field> <field name="team_id" ref="sales_team.crm_team_1"/> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead4"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="source_id" ref="crm_source_mailing"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="source_id" ref="utm.utm_source_mailing"/> </record> <record id="crm_case_21" model="crm.lead"> @@ -516,9 +516,9 @@ Andrew</field> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="crm.stage_lead4"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_services"/> - <field name="medium_id" ref="crm_medium_phone"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/> + <field name="medium_id" ref="utm.utm_medium_phone"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_22" model="crm.lead"> @@ -534,9 +534,9 @@ Andrew</field> <field name="team_id" ref="sales_team.crm_team_2"/> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="crm.stage_lead5"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_services"/> - <field name="medium_id" ref="crm_medium_email"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_23" model="crm.lead"> @@ -551,9 +551,9 @@ Andrew</field> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead5"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_fall_drive"/> - <field name="medium_id" ref="crm_medium_direct"/> - <field name="source_id" ref="crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_fall_drive"/> + <field name="medium_id" ref="utm.utm_medium_direct"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> <record id="crm_case_24" model="crm.lead"> @@ -571,9 +571,9 @@ Andrew</field> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="crm.stage_lead5"/> <field name="date_closed" eval="(DateTime.today() - relativedelta(month=1)).strftime('%Y-%m-%d %H:%M')"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_services"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_25" model="crm.lead"> @@ -595,8 +595,8 @@ Andrew</field> <field name="team_id" ref="sales_team.crm_team_1"/> <field name="user_id" ref="base.user_root"/> <field name="stage_id" ref="crm.stage_lead5"/> - <field name="campaign_id" ref="crm_campaign_email_campaign_services"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_services"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_26" model="crm.lead"> @@ -620,9 +620,9 @@ Andrew</field> <field name="user_id" ref="base.user_demo"/> <field name="stage_id" ref="crm.stage_lead5"/> <!-- <field name="date_closed" eval="(DateTime.today() - relativedelta(hours=1)).strftime('%Y-%m-%d %H:%M')"/> --> - <field name="campaign_id" ref="crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm_medium_website"/> - <field name="source_id" ref="crm_source_search_engine"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_website"/> + <field name="source_id" ref="utm.utm_source_search_engine"/> </record> <record id="crm_case_27" model="crm.lead"> diff --git a/addons/crm/crm_view.xml b/addons/crm/crm_view.xml index fa8918ffa55..5b133e9004c 100644 --- a/addons/crm/crm_view.xml +++ b/addons/crm/crm_view.xml @@ -23,90 +23,6 @@ <menuitem id="menu_crm_config_phonecall" name="Phone Calls" parent="base.menu_base_config" sequence="45" groups="base.group_sale_salesman"/> - <!-- crm.tracking.medium --> - <record id="crm_tracking_medium_view_tree" model="ir.ui.view"> - <field name="name">crm.tracking.medium.tree</field> - <field name="model">crm.tracking.medium</field> - <field name="arch" type="xml"> - <tree string="Channels"> - <field name="name"/> - <field name="active"/> - </tree> - </field> - </record> - <record id="crm_tracking_medium_view_form" model="ir.ui.view"> - <field name="name">crm.tracking.medium.form</field> - <field name="model">crm.tracking.medium</field> - <field name="arch" type="xml"> - <form string="Channel"> - <group> - <field name="name"/> - <field name="active"/> - </group> - </form> - </field> - </record> - <record id="crm_tracking_medium_action" model="ir.actions.act_window"> - <field name="name">Channels</field> - <field name="res_model">crm.tracking.medium</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="help" type="html"> - <p class="oe_view_nocontent_create"> - Click to define a new channel. - </p><p> - Use channels to track the type of source of your leads and opportunities. Channels - are mostly used in reporting to analyse sales performance - related to marketing efforts. - </p><p> - Some examples of channels: company website, phone call - campaign, reseller, banner, etc. - </p> - </field> - </record> - - <!-- crm.tracking.source --> - <record id="crm_tracking_source_view_tree" model="ir.ui.view"> - <field name="name">crm.tracking.source.tree</field> - <field name="model">crm.tracking.source</field> - <field name="arch" type="xml"> - <tree string="Channels"> - <field name="name"/> - </tree> - </field> - </record> - <record id="crm_tracking_source_view_form" model="ir.ui.view"> - <field name="name">crm.tracking.source.form</field> - <field name="model">crm.tracking.source</field> - <field name="arch" type="xml"> - <form string="Channel"> - <group> - <field name="name"/> - </group> - </form> - </field> - </record> - <record id="crm_tracking_source_action" model="ir.actions.act_window"> - <field name="name">Source</field> - <field name="res_model">crm.tracking.source</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="help" type="html"> - <p class="oe_view_nocontent_create"> - Click to define a new source. - </p><p> - Use channels to track the source of your leads and opportunities. Channels - are mostly used in reporting to analyse sales performance - related to marketing efforts. - </p><p> - Some examples of channels: partner's website, google.com, - email list name, etc. - </p> - </field> - </record> - - <menuitem action="crm_tracking_source_action" id="menu_crm_tracking_source" parent="base.menu_crm_config_lead" sequence="4" groups="base.group_no_one"/> - <!-- CRM.LOST.REASON VIEWS --> <record id="crm_lost_reason_view_form" model="ir.ui.view"> <field name="name">crm.lost.reason.form</field> @@ -180,55 +96,11 @@ </record> <!-- CRM Stage Action --> - <record id="crm_stage_act" model="ir.actions.act_window"> <field name="name">Stages</field> <field name="res_model">crm.stage</field> <field name="view_type">form</field> <field name="view_id" ref="crm_stage_tree"/> </record> - - <!-- Resource Type of case Tree View --> - - <record model="ir.ui.view" id="crm_tracking_campaign_tree"> - <field name="name">crm.tracking.campaign.tree</field> - <field name="model">crm.tracking.campaign</field> - <field name="arch" type="xml"> - <tree string="Campaign"> - <field name="name"/> - <field name="team_id" groups="base.group_multi_salesteams"/> - </tree> - </field> - </record> - - <!-- Resource Type of case Form View --> - - <record model="ir.ui.view" id="crm_tracking_campaign_form"> - <field name="name">crm.tracking.campaign.form</field> - <field name="model">crm.tracking.campaign</field> - <field name="arch" type="xml"> - <form string="Campaign"> - <group> - <field name="name"/> - <field name="team_id" groups="base.group_multi_salesteams"/> - </group> - </form> - </field> - </record> - - <!-- Resource Type of case Action --> - - <record id="crm_tracking_campaign_act" model="ir.actions.act_window"> - <field name="name">Campaigns</field> - <field name="res_model">crm.tracking.campaign</field> - <field name="view_type">form</field> - <field name="view_id" ref="crm_tracking_campaign_tree"/> - </record> - - <menuitem action="crm_tracking_campaign_act" - id="menu_crm_tracking_campaign_act" sequence="4" - groups="base.group_no_one" - parent="base.menu_crm_config_lead"/> - </data> </openerp> diff --git a/addons/crm/ir_http.py b/addons/crm/ir_http.py index c3e1f445e9a..7efd80f0737 100644 --- a/addons/crm/ir_http.py +++ b/addons/crm/ir_http.py @@ -11,7 +11,7 @@ class ir_http(orm.AbstractModel): def _dispatch(self): response = super(ir_http, self)._dispatch() - for var, dummy in self.pool['crm.tracking.mixin'].tracking_fields(): + for var, dummy in self.pool['utm.mixin'].tracking_fields(): if var in request.params and (var not in request.session or request.session[var] != request.params[var]): response.set_cookie(var, request.params[var], domain=self.get_utm_domain_cookies()) return response diff --git a/addons/crm/report/crm_lead_report.py b/addons/crm/report/crm_lead_report.py index 7a398d738dc..4c68d2198e3 100644 --- a/addons/crm/report/crm_lead_report.py +++ b/addons/crm/report/crm_lead_report.py @@ -29,7 +29,7 @@ class crm_lead_report(osv.Model): _auto = False _description = "CRM Lead Analysis" _rec_name = 'date_deadline' - _inherit = ["crm.tracking.mixin"] + _inherit = ["utm.mixin"] _columns = { 'date_deadline': fields.date('Expected Closing', readonly=True), diff --git a/addons/crm/report/crm_opportunity_report.py b/addons/crm/report/crm_opportunity_report.py index 3db5bd7ef16..376ff44c3a4 100644 --- a/addons/crm/report/crm_opportunity_report.py +++ b/addons/crm/report/crm_opportunity_report.py @@ -30,7 +30,7 @@ class crm_opportunity_report(osv.Model): _auto = False _description = "CRM Opportunity Analysis" _rec_name = 'date_deadline' - _inherit = ["crm.tracking.mixin"] + _inherit = ["utm.mixin"] _columns = { 'date_deadline': fields.date('Expected Closing', readonly=True), diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv index fff272c55de..09150132f14 100644 --- a/addons/crm/security/ir.model.access.csv +++ b/addons/crm/security/ir.model.access.csv @@ -1,16 +1,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_crm_tracking_medium_user,crm.tracking.medium user,model_crm_tracking_medium,base.group_sale_salesman,1,0,0,0
-access_crm_tracking_medium_manager,crm.tracking.medium manager,model_crm_tracking_medium,base.group_sale_manager,1,1,1,1
-access_crm_tracking_source_user,crm.tracking.source user,model_crm_tracking_source,base.group_sale_salesman,1,0,0,0
-access_crm_tracking_source_manager,crm.tracking.source manager,model_crm_tracking_source,base.group_sale_manager,1,1,1,1
access_crm_lead_manager,crm.lead.manager,model_crm_lead,base.group_sale_manager,1,1,1,1
access_crm_phonecall_manager,crm.phonecall.manager,model_crm_phonecall,base.group_sale_manager,1,1,1,1
access_crm_lead,crm.lead,model_crm_lead,base.group_sale_salesman,1,1,1,0
access_crm_phonecall,crm.phonecall,model_crm_phonecall,base.group_sale_salesman,1,1,1,0
access_crm_stage,crm.stage,model_crm_stage,,1,0,0,0
access_crm_stage_manager,crm.stage,model_crm_stage,base.group_sale_manager,1,1,1,1
-access_crm_tracking_campaign_user,crm_tracking_campaign user,model_crm_tracking_campaign,base.group_sale_salesman,1,1,1,0
-access_crm_tracking_campaign_manager,crm_tracking_campaign manager,model_crm_tracking_campaign,base.group_sale_manager,1,1,1,1
access_crm_phonecall_report_user,crm.phonecall.report.user,model_crm_phonecall_report,base.group_sale_salesman,1,0,0,0
access_crm_phonecall_report_manager,crm.phonecall.report,model_crm_phonecall_report,base.group_sale_manager,1,1,1,1
access_res_partner_manager,res.partner.crm.manager,base.model_res_partner,base.group_sale_manager,1,0,0,0
diff --git a/addons/crm_claim/crm_claim_data.xml b/addons/crm_claim/crm_claim_data.xml index 44eb291acd6..8f0462f091e 100644 --- a/addons/crm_claim/crm_claim_data.xml +++ b/addons/crm_claim/crm_claim_data.xml @@ -25,14 +25,12 @@ Case Category2 --> - <record model="crm.tracking.campaign" id="claim_source1"> + <record model="utm.campaign" id="claim_source1"> <field name="name">Corrective</field> - <field name="team_id" ref="sales_team.team_sales_department"/> </record> - <record model="crm.tracking.campaign" id="claim_source2"> + <record model="utm.campaign" id="claim_source2"> <field name="name">Preventive</field> - <field name="team_id" ref="sales_team.team_sales_department"/> </record> <!-- diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py index 6286a025ae9..0069b660204 100644 --- a/addons/crm_helpdesk/crm_helpdesk.py +++ b/addons/crm_helpdesk/crm_helpdesk.py @@ -56,7 +56,7 @@ class crm_helpdesk(osv.osv): 'date': fields.datetime('Date'), 'ref': fields.reference('Reference', selection=openerp.addons.base.res.res_request.referencable_models), 'ref2': fields.reference('Reference 2', selection=openerp.addons.base.res.res_request.referencable_models), - 'channel_id': fields.many2one('crm.tracking.medium', 'Channel', help="Communication channel."), + 'channel_id': fields.many2one('utm.medium', 'Channel', help="Communication channel."), 'planned_revenue': fields.float('Planned Revenue'), 'planned_cost': fields.float('Planned Costs'), 'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], 'Priority'), diff --git a/addons/crm_helpdesk/report/crm_helpdesk_report.py b/addons/crm_helpdesk/report/crm_helpdesk_report.py index c75f0de3178..1f527e9db23 100644 --- a/addons/crm_helpdesk/report/crm_helpdesk_report.py +++ b/addons/crm_helpdesk/report/crm_helpdesk_report.py @@ -50,7 +50,7 @@ class crm_helpdesk_report(osv.osv): 'date_deadline': fields.date('Deadline', select=True), 'priority': fields.selection([('5', 'Lowest'), ('4', 'Low'), \ ('3', 'Normal'), ('2', 'High'), ('1', 'Highest')], 'Priority'), - 'channel_id': fields.many2one('crm.tracking.medium', 'Channel'), + 'channel_id': fields.many2one('utm.medium', 'Channel'), 'categ_id': fields.many2one('crm.helpdesk.category', 'Category'), 'planned_cost': fields.float('Planned Costs'), 'create_date': fields.datetime('Creation Date' , readonly=True, select=True), diff --git a/addons/crm_mass_mailing/__init__.py b/addons/crm_mass_mailing/__init__.py deleted file mode 100644 index 983faeb18a6..00000000000 --- a/addons/crm_mass_mailing/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2014-Today OpenERP SA (<http://www.openerp.com>). -# -# 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/>. -# -############################################################################## - -import mass_mailing diff --git a/addons/crm_mass_mailing/__openerp__.py b/addons/crm_mass_mailing/__openerp__.py deleted file mode 100644 index 5e2e320f9f0..00000000000 --- a/addons/crm_mass_mailing/__openerp__.py +++ /dev/null @@ -1,18 +0,0 @@ -{ - 'name': 'Campaign in Mass Mailing', - 'version': '1.0', - 'summary': 'This module allow to specify a campaign, a source and a channel for a mass mailing campaign.', - 'author': 'OpenERP SA', - 'description': """ -Mass Mailing with Crm Marketing -================================ - -Link module mass mailing with the marketing mixin from crm. - """, - 'depends': ['crm', 'mass_mailing'], - 'data': [ - 'mass_mailing.xml', - ], - 'installable': True, - 'auto_install': True, -} diff --git a/addons/crm_mass_mailing/i18n/ar.po b/addons/crm_mass_mailing/i18n/ar.po deleted file mode 100644 index f982c1c8b4c..00000000000 --- a/addons/crm_mass_mailing/i18n/ar.po +++ /dev/null @@ -1,62 +0,0 @@ -# Arabic translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-11-14 18:16+0000\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: Arabic <ar@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-11-15 07:01+0000\n" -"X-Generator: Launchpad (build 17241)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "" diff --git a/addons/crm_mass_mailing/i18n/bg.po b/addons/crm_mass_mailing/i18n/bg.po deleted file mode 100644 index 2983f95515e..00000000000 --- a/addons/crm_mass_mailing/i18n/bg.po +++ /dev/null @@ -1,62 +0,0 @@ -# Bulgarian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-11-18 14:41+0000\n" -"Last-Translator: Dimitar Dimitrov <dimitrovden@gmail.com>\n" -"Language-Team: Bulgarian <bg@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-11-19 06:42+0000\n" -"X-Generator: Launchpad (build 17252)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Кампания" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Канал" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Изпращане на масови имейл съобщения" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Източник" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Проследяване" diff --git a/addons/crm_mass_mailing/i18n/crm_mass_mailing.pot b/addons/crm_mass_mailing/i18n/crm_mass_mailing.pot deleted file mode 100644 index 67e4b0e15be..00000000000 --- a/addons/crm_mass_mailing/i18n/crm_mass_mailing.pot +++ /dev/null @@ -1,57 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * crm_mass_mailing -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0rc1\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-08-14 13:09+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "This is a name that helps you keep track of your different campaign efforts Ex: Fall_Drive, Christmas_Special" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "This is the source of the link Ex: Search Engine, another domain, or name of email list" -msgstr "" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "" - diff --git a/addons/crm_mass_mailing/i18n/de.po b/addons/crm_mass_mailing/i18n/de.po deleted file mode 100644 index 03adabe8c32..00000000000 --- a/addons/crm_mass_mailing/i18n/de.po +++ /dev/null @@ -1,68 +0,0 @@ -# German translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-08-28 10:06+0000\n" -"Last-Translator: Thorsten Vocks (OpenBig.org) <thorsten.vocks@big-" -"consulting.net>\n" -"Language-Team: German <de@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-08-30 08:37+0000\n" -"X-Generator: Launchpad (build 17176)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampagne" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Vertriebsweg" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Massenmailing" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Herkunft" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Diese Bezeichnung hilft Ihnen bei der Analyse Ihres Kampagnenerfolgs, z.B.: " -"Herbstangebot, Weihnachtsangebot" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" -"Dies ist die Kommunikationsmethode: z.B. Postkarte, E-Mail oder Bannerwerbung" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Dies zeigt die Herkunft für neue Interessenten: Suchmaschine, andere " -"Webdomäne, E-Mail Liste" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Verfolgung" diff --git a/addons/crm_mass_mailing/i18n/es.po b/addons/crm_mass_mailing/i18n/es.po deleted file mode 100644 index 1cc554c926c..00000000000 --- a/addons/crm_mass_mailing/i18n/es.po +++ /dev/null @@ -1,68 +0,0 @@ -# Spanish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-08-18 11:59+0000\n" -"Last-Translator: Pedro Manuel Baeza <pedro.baeza@gmail.com>\n" -"Language-Team: Spanish <es@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-08-19 07:04+0000\n" -"X-Generator: Launchpad (build 17156)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Campaña" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Canal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Envío masivo" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Origen" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Éste es un nombre que facilita la búsqueda de los diferentes esfuerzos de " -"campañas. Por ejemplo: campaña de Navidad, rebajas de otoño" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" -"Éste es el método de envío. Por ejemplo: carta postal, correo electrónico, " -"publicidad web" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Ésta es la fuente del enlace. Por ejemplo: motor de búsqueda, otro dominio o " -"nombre de unos lista de correos" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Seguimiento" diff --git a/addons/crm_mass_mailing/i18n/hr.po b/addons/crm_mass_mailing/i18n/hr.po deleted file mode 100644 index aa7d866ab97..00000000000 --- a/addons/crm_mass_mailing/i18n/hr.po +++ /dev/null @@ -1,62 +0,0 @@ -# Croatian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-09-01 08:24+0000\n" -"Last-Translator: Goran Kliska <gkliska@gmail.com>\n" -"Language-Team: Croatian <hr@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-02 07:33+0000\n" -"X-Generator: Launchpad (build 17192)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampanja" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Kanal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Grupna e-pošta" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Izvori" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Praćenje" diff --git a/addons/crm_mass_mailing/i18n/hu.po b/addons/crm_mass_mailing/i18n/hu.po deleted file mode 100644 index 1bddae4a1fc..00000000000 --- a/addons/crm_mass_mailing/i18n/hu.po +++ /dev/null @@ -1,66 +0,0 @@ -# Hungarian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-06 21:19+0000\n" -"Last-Translator: krnkris <Unknown>\n" -"Language-Team: Hungarian <hu@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-07 07:02+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampány" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Csatorna" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Tömeges email küldés" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Forrás" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Ez a név ami segít követni a különböző kampány törekvéseit. Pl: " -"Őszi_árhullás, Karácsonyi_speciál" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Ez a kivitelezés módja. Ex: Posta, Email, vagy Transzparens hirdetés" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Ez az elérési út forrása. Pl: Kereső motor, másik domain, vagy email lista " -"neve" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Nyomonkövetés" diff --git a/addons/crm_mass_mailing/i18n/it.po b/addons/crm_mass_mailing/i18n/it.po deleted file mode 100644 index f1662cdc62d..00000000000 --- a/addons/crm_mass_mailing/i18n/it.po +++ /dev/null @@ -1,67 +0,0 @@ -# Italian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-01 20:16+0000\n" -"Last-Translator: Nicola Riolini - Micronaet <nicola.riolini@gmail.com>\n" -"Language-Team: Italian <it@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-02 06:42+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Campagna" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Canale" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Mailing di Massa" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Sorgente" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Questo nome puo' aiutarti a tenere traccia di differenti campagne di " -"vendita. Es. Sconti natalizi, saldi autunnali, ecc.." - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" -"Rappresenta il metodo di consegna. Es: email, banner, volantini, ecc.." - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Questo è il sorgente del link, es.: Motore di ricerca, altro dominio o nome " -"della e-mail list" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Tracciabilità" diff --git a/addons/crm_mass_mailing/i18n/ko.po b/addons/crm_mass_mailing/i18n/ko.po deleted file mode 100644 index d392dd8a182..00000000000 --- a/addons/crm_mass_mailing/i18n/ko.po +++ /dev/null @@ -1,63 +0,0 @@ -# Korean translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-09-02 08:30+0000\n" -"Last-Translator: Josh Kim <joshkkim@gmail.com>\n" -"Language-Team: Korean <ko@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-03 07:29+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "캠페인" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "채널" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "대량 메일 발송" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "소스" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"이것은 다른 캠페인 노력을 추적할 때 도움을 주는 이름입니다. 예시: Fall_Drive, Christmas_Special" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "이것은 전달방식입니다. 예시: 엽서, 이메일 또는 배너광고" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "이것은 링크의 소스입니다. 예시: 검색엔진, 다른 도메인 또는 이메일 목록의 이름" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "추적" diff --git a/addons/crm_mass_mailing/i18n/lv.po b/addons/crm_mass_mailing/i18n/lv.po deleted file mode 100644 index 9045774b23e..00000000000 --- a/addons/crm_mass_mailing/i18n/lv.po +++ /dev/null @@ -1,62 +0,0 @@ -# Latvian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-23 11:32+0000\n" -"Last-Translator: Vladimirs Kuzmins <Unknown>\n" -"Language-Team: Latvian <lv@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-24 07:05+0000\n" -"X-Generator: Launchpad (build 17203)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampaņa" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Kanāls" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Masveida e-pastu sūtīšana" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Avots" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Izsekošana" diff --git a/addons/crm_mass_mailing/i18n/mk.po b/addons/crm_mass_mailing/i18n/mk.po deleted file mode 100644 index 498cfc81b58..00000000000 --- a/addons/crm_mass_mailing/i18n/mk.po +++ /dev/null @@ -1,65 +0,0 @@ -# Macedonian translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-17 08:36+0000\n" -"Last-Translator: Tome Barbov <tome.barbov@eskon.com.mk>\n" -"Language-Team: Macedonian <mk@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-18 07:21+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Кампања" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Канал" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Масовно праќање пошта" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Извор" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Ова е име што ви помага да следите резултати од различни кампањи пр. : " -"Fall_Drive, Christmas_Special" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Ова е начин на испорака. Пр. Честитка, Е-маил или онлајн реклама" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"ова е извор на врската пр. : пребарувач, друг домен или име од е-маил листа" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Следење" diff --git a/addons/crm_mass_mailing/i18n/nl.po b/addons/crm_mass_mailing/i18n/nl.po deleted file mode 100644 index 9f9e9e2b2a3..00000000000 --- a/addons/crm_mass_mailing/i18n/nl.po +++ /dev/null @@ -1,68 +0,0 @@ -# Dutch translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-08-14 19:27+0000\n" -"Last-Translator: Erwin van der Ploeg (BAS Solutions) <Unknown>\n" -"Language-Team: Dutch <nl@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-08-15 07:03+0000\n" -"X-Generator: Launchpad (build 17156)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Campagne" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Kanaal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Bulk-mailing" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Bron" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Dit is de naam welke u helpt om uw verschillende campagnes te volgen. Zoals " -"bijvoorbeeld: Fall_drive, Christmas_Special" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "" -"Dit is de methode van afleveren. Bijvoorbeeld, Brief, E-mail of Banner " -"advertantie" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Dit is de bron van de link. Bijvoorbeeld: Search engine, ander domein of de " -"naam van de e-mail lijst" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Volgen" diff --git a/addons/crm_mass_mailing/i18n/pl.po b/addons/crm_mass_mailing/i18n/pl.po deleted file mode 100644 index f23905d9835..00000000000 --- a/addons/crm_mass_mailing/i18n/pl.po +++ /dev/null @@ -1,62 +0,0 @@ -# Polish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-08-29 14:57+0000\n" -"Last-Translator: Grzegorz Grzelak (OpenGLOBE.pl) <grzegorz@openglobe.pl>\n" -"Language-Team: Polish <pl@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-08-30 08:37+0000\n" -"X-Generator: Launchpad (build 17176)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampania" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Kanał" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Poczta masowa" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Źródło" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Metoda dostawy. Np: pocztówka, e-mail, lub baner" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "Źródło adresu np: wyszukiwarka, inna domena lub e-mail" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Śledzenie" diff --git a/addons/crm_mass_mailing/i18n/pt.po b/addons/crm_mass_mailing/i18n/pt.po deleted file mode 100644 index 841f99730e8..00000000000 --- a/addons/crm_mass_mailing/i18n/pt.po +++ /dev/null @@ -1,66 +0,0 @@ -# Portuguese translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-15 15:51+0000\n" -"Last-Translator: Daniel Santos - ThinkOpen <dcs@thinkopensolutions.pt>\n" -"Language-Team: Portuguese <pt@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-16 05:52+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Campanha" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Canal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Emails em Massa" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Origem" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Este é um nome que o ajuda a acompanhar os seus esforços das diferentes " -"campanhas Ex: Natal_Especial, etc.." - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Este é o método de entrega. Ex: Cartão postal, e-mail ou anúncio web" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Esta é a origem do link Ex: Motor de Busca, um outro domínio ou nome de " -"lista de e-mail" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Rastrear" diff --git a/addons/crm_mass_mailing/i18n/pt_BR.po b/addons/crm_mass_mailing/i18n/pt_BR.po deleted file mode 100644 index c8d354bfcb1..00000000000 --- a/addons/crm_mass_mailing/i18n/pt_BR.po +++ /dev/null @@ -1,67 +0,0 @@ -# Brazilian Portuguese translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-09-06 02:02+0000\n" -"Last-Translator: Fábio Martinelli - http://zupy.com.br " -"<webmaster@zupy.com.br>\n" -"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-09-06 08:25+0000\n" -"X-Generator: Launchpad (build 17196)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Campanha" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Canal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Email em Massa" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Origem" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Este é o nome que te ajuda a rastrear as diferentes campanhas Ex: Especial " -"de Natal, Páscoa" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Este é o método de entrega. Ex: Cartão Postal, Email, Banner" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Esta é a origem do link. Ex: Site de Busca, outro domínio, ou nome da lista " -"de emails" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Rastreamento" diff --git a/addons/crm_mass_mailing/i18n/sv.po b/addons/crm_mass_mailing/i18n/sv.po deleted file mode 100644 index d3fc8f1d41a..00000000000 --- a/addons/crm_mass_mailing/i18n/sv.po +++ /dev/null @@ -1,66 +0,0 @@ -# Swedish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-20 19:27+0000\n" -"Last-Translator: Fredrik Arvas <fredrik.arvas@smart-eu.org>\n" -"Language-Team: Swedish <sv@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-21 07:53+0000\n" -"X-Generator: Launchpad (build 17203)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampanj" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Kanal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Massutskick" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Källa" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Det här är ett namn som hjälper dig hålla ordning på olika kampanjer. Ex. " -"Höst_körning, Jul_Special" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Det här är leverensmetoden. Ex. Vykort, Epost eller Banner" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Det här är källan till länken. Ex: Sökmotor, en annan domän eller namn på " -"mailinglista" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "Spårning" diff --git a/addons/crm_mass_mailing/i18n/tr.po b/addons/crm_mass_mailing/i18n/tr.po deleted file mode 100644 index e9aad6d1dda..00000000000 --- a/addons/crm_mass_mailing/i18n/tr.po +++ /dev/null @@ -1,66 +0,0 @@ -# Turkish translation for openobject-addons -# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 -# This file is distributed under the same license as the openobject-addons package. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. -# -msgid "" -msgstr "" -"Project-Id-Version: openobject-addons\n" -"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n" -"POT-Creation-Date: 2014-08-14 13:09+0000\n" -"PO-Revision-Date: 2014-10-28 09:41+0000\n" -"Last-Translator: Ayhan KIZILTAN <Unknown>\n" -"Language-Team: Turkish <tr@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2014-10-29 07:47+0000\n" -"X-Generator: Launchpad (build 17203)\n" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,campaign_id:0 -msgid "Campaign" -msgstr "Kampanya" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,medium_id:0 -msgid "Channel" -msgstr "Kanal" - -#. module: crm_mass_mailing -#: model:ir.model,name:crm_mass_mailing.model_mail_mass_mailing -msgid "Mass Mailing" -msgstr "Toplu Mail" - -#. module: crm_mass_mailing -#: field:mail.mass_mailing,source_id:0 -msgid "Source" -msgstr "Kaynak" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,campaign_id:0 -msgid "" -"This is a name that helps you keep track of your different campaign efforts " -"Ex: Fall_Drive, Christmas_Special" -msgstr "" -"Farklı kampanya çalışmalarınızı kaydını tutmaya yarayan bir isimdir. Örn: " -"Güz_Sürüşü, Yılbaşı_Özel" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,medium_id:0 -msgid "This is the method of delivery. Ex: Postcard, Email, or Banner Ad" -msgstr "Bu teslimat yöntemidir. Örn: Postakartı, Eposta yada Reklam Afişi" - -#. module: crm_mass_mailing -#: help:mail.mass_mailing,source_id:0 -msgid "" -"This is the source of the link Ex: Search Engine, another domain, or name of " -"email list" -msgstr "" -"Bu, bağlantının kaynağıdır. Örn: Arama Motoru, başka alan adı yada bir " -"eposta listesi adı" - -#. module: crm_mass_mailing -#: view:mail.mass_mailing:crm_mass_mailing.view_tracking_mass_mailing_form -msgid "Tracking" -msgstr "İzleme" diff --git a/addons/crm_mass_mailing/mass_mailing.py b/addons/crm_mass_mailing/mass_mailing.py deleted file mode 100644 index 6a2e1322136..00000000000 --- a/addons/crm_mass_mailing/mass_mailing.py +++ /dev/null @@ -1,6 +0,0 @@ -from openerp.osv import osv - - -class MassMailing(osv.Model): - _name = 'mail.mass_mailing' - _inherit = ['mail.mass_mailing', 'crm.tracking.mixin'] diff --git a/addons/crm_mass_mailing/mass_mailing.xml b/addons/crm_mass_mailing/mass_mailing.xml deleted file mode 100644 index a6768e7be84..00000000000 --- a/addons/crm_mass_mailing/mass_mailing.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0"?> -<openerp> - <data> - <record model="ir.ui.view" id="view_tracking_mass_mailing_form"> - <field name="name">mail.mass_mailing.inherit.form</field> - <field name="model">mail.mass_mailing</field> - <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/> - - <field name="arch" type="xml"> - <xpath expr="//notebook/page[@string='Options']" position="after"> - <page string="Tracking"> - <group> - <field name="campaign_id" /> - <field name="medium_id" /> - <field name="source_id" /> - </group> - </page> - </xpath> - </field> - </record> - </data> -</openerp> - diff --git a/addons/crm_mass_mailing/mass_mailing_data.xml b/addons/crm_mass_mailing/mass_mailing_data.xml deleted file mode 100644 index e69de29bb2d..00000000000 --- a/addons/crm_mass_mailing/mass_mailing_data.xml +++ /dev/null diff --git a/addons/crm_partner_assign/crm_lead_demo.xml b/addons/crm_partner_assign/crm_lead_demo.xml index 56a2f9c6211..6264f8ebb3f 100644 --- a/addons/crm_partner_assign/crm_lead_demo.xml +++ b/addons/crm_partner_assign/crm_lead_demo.xml @@ -25,9 +25,9 @@ Steve</field> <field eval="1" name="active"/> <field name="partner_assigned_id" ref="portal.partner_demo_portal"/> - <field name="campaign_id" ref="crm.crm_campaign_email_campaign_products"/> - <field name="medium_id" ref="crm.crm_medium_email"/> - <field name="source_id" ref="crm.crm_source_newsletter"/> + <field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/> + <field name="medium_id" ref="utm.utm_medium_email"/> + <field name="source_id" ref="utm.utm_source_newsletter"/> </record> </data> </openerp>
\ No newline at end of file diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index caecb47f564..6842c863be3 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -181,7 +181,7 @@ class mail_mail(osv.Model): def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): """Return a specific ir_email body. The main purpose of this method is to be inherited to add custom content depending on some module.""" - body = mail.body_html + body = mail.body_html or '' # generate access links for notifications or emails linked to a specific document with auto threading link = None diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py index 489fc71ab96..bb7d8b39452 100644 --- a/addons/mass_mailing/__openerp__.py +++ b/addons/mass_mailing/__openerp__.py @@ -38,6 +38,8 @@ professional emails and reuse templates in a few clicks. 'web_kanban_gauge', 'web_kanban_sparkline', 'website_mail', + 'website_links', + 'utm', ], 'data': [ 'data/mail_data.xml', diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 69317cb08c9..05077be0744 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -15,9 +15,10 @@ class MassMailController(http.Controller): response = werkzeug.wrappers.Response() response.mimetype = 'image/gif' response.data = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='.decode('base64') + return response - @http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', auth='none') + @http.route('/mail/mailing/<int:mailing_id>/unsubscribe', type='http', auth='none') def mailing(self, mailing_id, email=None, res_id=None, **post): cr, uid, context = request.cr, request.uid, request.context MassMailing = request.registry['mail.mass_mailing'] @@ -42,7 +43,7 @@ class MassMailController(http.Controller): model.write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context) return 'OK' - @http.route(['/website_mass_mailing/is_subscriber'], type='json', auth="public", website=True) + @http.route('/website_mass_mailing/is_subscriber', type='json', auth="public", website=True) def is_subscriber(self, list_id, **post): cr, uid, context = request.cr, request.uid, request.context Contacts = request.registry['mail.mass_mailing.contact'] @@ -61,7 +62,7 @@ class MassMailController(http.Controller): return {'is_subscriber': is_subscriber, 'email': email} - @http.route(['/website_mass_mailing/subscribe'], type='json', auth="public", website=True) + @http.route('/website_mass_mailing/subscribe', type='json', auth="public", website=True) def subscribe(self, list_id, email, **post): cr, uid, context = request.cr, request.uid, request.context Contacts = request.registry['mail.mass_mailing.contact'] @@ -75,3 +76,9 @@ class MassMailController(http.Controller): # add email to session request.session['mass_mailing_email'] = email return True + + @http.route('/r/<string:code>/m/<int:stat_id>', type='http', auth="none") + def full_url_redirect(self, code, stat_id, **post): + cr, uid, context = request.cr, request.uid, request.context + request.registry['website.links.click'].add_click(cr, uid, code, request.httprequest.remote_addr, request.session['geoip'].get('country_code'), stat_id=stat_id, context=context) + return werkzeug.utils.redirect(request.registry['website.links'].get_url_from_code(cr, uid, code, context=context), 301) diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index 6157be3fbe1..057db04052b 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -97,6 +97,9 @@ <field name="stage_id" ref="mass_mailing.campaign_stage_1"/> <field name="user_id" eval="ref('base.user_root')"/> <field name="category_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_category_1')])]"/> + <field name="campaign_id" eval="ref('utm.utm_campaign_email_campaign_products')"/> + <field name="medium_id" eval="ref('utm.utm_medium_email')"/> + <field name="source_id" eval="ref('utm.utm_source_mailing')"/> </record> <record id="mass_mail_1" model="mail.mass_mailing"> diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py index 702da268d84..7cc1ba680b4 100644 --- a/addons/mass_mailing/models/__init__.py +++ b/addons/mass_mailing/models/__init__.py @@ -6,3 +6,4 @@ import mail_mail import mail_thread import res_config import mass_mailing_report +import website_links_click diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 0e5f7821a9a..555b10725b6 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -4,14 +4,17 @@ from datetime import datetime from dateutil import relativedelta import json import random +import re from openerp import tools +from openerp import models, api, _ from openerp.exceptions import Warning from openerp.tools.safe_eval import safe_eval as eval -from openerp.tools.translate import _ from openerp.tools import ustr from openerp.osv import osv, fields +URL_REGEX = r'https?://[^\s<>"]+|www\.[^\s<>"]+' + class MassMailingCategory(osv.Model): """Model of categories of mass mailing, i.e. marketing, newsletter, ... """ @@ -148,6 +151,9 @@ class MassMailingCampaign(osv.Model): _name = "mail.mass_mailing.campaign" _description = 'Mass Mailing Campaign' + _inherit = ['utm.mixin'] + _inherits = {'utm.campaign': 'campaign_id'} + def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ results = {} @@ -181,6 +187,21 @@ class MassMailingCampaign(osv.Model): row['replied_ratio'] = 100.0 * row['replied'] / total return results + def _get_clicks_ratio(self, cr, uid, ids, name, arg, context=None): + res = dict.fromkeys(ids, 0) + cr.execute(""" + SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id + FROM mail_mail_statistics AS stats + LEFT OUTER JOIN website_links_click AS clicks ON clicks.mail_stat_id = stats.id + WHERE stats.mass_mailing_campaign_id IN %s + GROUP BY stats.mass_mailing_campaign_id + """, (tuple(ids), )) + + for record in cr.dictfetchall(): + res[record['id']] = 100 * record['nb_clicks'] / record['nb_mails'] + + return res + _columns = { 'name': fields.char('Name', required=True), 'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True), @@ -188,6 +209,8 @@ class MassMailingCampaign(osv.Model): 'res.users', 'Responsible', required=True, ), + 'campaign_id': fields.many2one('utm.campaign', 'campaign_id', + required=True, ondelete='cascade'), 'category_ids': fields.many2many( 'mail.mass_mailing.category', 'mail_mass_mailing_category_rel', 'category_id', 'campaign_id', string='Categories'), @@ -201,6 +224,10 @@ class MassMailingCampaign(osv.Model): 'various mailings in a single campaign to test the effectiveness' 'of the mailings.'), 'color': fields.integer('Color Index'), + 'clicks_ratio': fields.function( + _get_clicks_ratio, string="Number of clicks", + type="integer", + ), # stat fields 'total': fields.function( _get_statistics, string='Total', @@ -252,11 +279,24 @@ class MassMailingCampaign(osv.Model): stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context) return stage_ids and stage_ids[0] or False + def _get_source_id(self, cr, uid, context=None): + return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'utm.utm_source_newsletter') + + def _get_medium_id(self, cr, uid, context=None): + return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'utm.utm_medium_email') + _defaults = { 'user_id': lambda self, cr, uid, ctx=None: uid, 'stage_id': lambda self, *args: self._get_default_stage_id(*args), + 'source_id': lambda self, *args: self._get_source_id(*args), + 'medium_id': lambda self,*args: self._get_medium_id(*args), } + def mass_mailing_statistics_action(self, cr, uid, ids, context=None): + res = self.pool['ir.actions.act_window'].for_xml_id(cr, uid, 'mass_mailing', 'action_view_mass_mailing_statistics', context=context) + res['domain'] = [('mass_mailing_campaign_id', 'in', ids)] + return res + def get_recipients(self, cr, uid, ids, model=None, context=None): """Return the recipients of a mailing campaign. This is based on the statistics build for each mailing. """ @@ -270,6 +310,17 @@ class MassMailingCampaign(osv.Model): res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context)) return res + def on_change_campaign_name(self, cr, uid, ids, name, context=None): + if name: + mass_mailing_campaign = self.browse(cr, uid, ids, context=context) + if mass_mailing_campaign.campaign_id: + utm_campaign_id = mass_mailing_campaign.campaign_id.id + self.pool['utm.campaign'].write(cr, uid, [utm_campaign_id], {'name': name}, context=context) + else: + utm_campaign_id = self.pool['utm.campaign'].create(cr, uid, {'name': name}, context=context) + + return {'value': {'campaign_id': utm_campaign_id}} + class MassMailing(osv.Model): """ MassMailing models a wave of emails for a mass mailign campaign. @@ -281,6 +332,8 @@ class MassMailing(osv.Model): _period_number = 6 _order = 'sent_date DESC' + _inherit = ['utm.mixin'] + def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, context=None): """ Generic method to generate data for bar chart values using SparklineBarWidget. This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field). @@ -370,6 +423,21 @@ class MassMailing(osv.Model): res.append(('mail.mass_mailing.contact', _('Mailing List'))) return res + def _get_clicks_ratio(self, cr, uid, ids, name, arg, context=None): + res = dict.fromkeys(ids, 0) + cr.execute(""" + SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id + FROM mail_mail_statistics AS stats + LEFT OUTER JOIN website_links_click AS clicks ON clicks.mail_stat_id = stats.id + WHERE stats.mass_mailing_id IN %s + GROUP BY stats.mass_mailing_id + """, (tuple(ids), )) + + for record in cr.dictfetchall(): + res[record['id']] = 100 * record['nb_clicks'] / record['nb_mails'] + + return res + # indirections for inheritance _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs) @@ -388,6 +456,10 @@ class MassMailing(osv.Model): 'mail.mass_mailing.campaign', 'Mass Mailing Campaign', ondelete='set null', ), + 'clicks_ratio': fields.function( + _get_clicks_ratio, string="Number of Clicks", + type="integer", + ), 'state': fields.selection( [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')], string='Status', required=True, copy=False, @@ -473,6 +545,12 @@ class MassMailing(osv.Model): ) } + def mass_mailing_statistics_action(self, cr, uid, ids, context=None): + res = self.pool['ir.actions.act_window'].for_xml_id(cr, uid, 'mass_mailing', 'action_view_mass_mailing_statistics', context=context) + link_click_ids = self.pool['website.links.click'].search(cr, uid, [('mass_mailing_id', 'in', ids)], context=context) + res['domain'] = [('id', 'in', link_click_ids)] + return res + def default_get(self, cr, uid, fields, context=None): res = super(MassMailing, self).default_get(cr, uid, fields, context=context) if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'): @@ -490,6 +568,16 @@ class MassMailing(osv.Model): 'contact_ab_pc': 100, } + def onchange_mass_mailing_campaign_id(self, cr, uid, id, mass_mailing_campaign_ids, context=None): + if mass_mailing_campaign_ids: + mass_mailing_campaign = self.pool['mail.mass_mailing.campaign'].browse(cr, uid, mass_mailing_campaign_ids, context=context) + + dic = {'campaign_id': mass_mailing_campaign[0].campaign_id.id, + 'source_id': mass_mailing_campaign[0].source_id.id, + 'medium_id': mass_mailing_campaign[0].medium_id.id} + print dic + return {'value': dic} + #------------------------------------------------------ # Technical stuff #------------------------------------------------------ @@ -559,6 +647,7 @@ class MassMailing(osv.Model): 'res_model': 'mail.mass_mailing', 'res_id': copy_id, 'context': context, + 'flags': {'initial_mode': 'edit'}, } return False @@ -622,7 +711,7 @@ class MassMailing(osv.Model): composer_values = { 'author_id': author_id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], - 'body': mailing.body_html, + 'body': self.convert_link(cr, uid, [mailing.id], context=context)[mailing.id], 'subject': mailing.name, 'model': mailing.mailing_model, 'email_from': mailing.email_from, @@ -638,3 +727,43 @@ class MassMailing(osv.Model): self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx) self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context) return True + + def convert_link(self, cr, uid, ids, context=None): + website_links = self.pool['website.links'] + res = {} + for mass_mailing in self.browse(cr, uid, ids, context=context): + res[mass_mailing.id] = mass_mailing.body_html if mass_mailing.body_html else '' + + for long_url in re.findall(URL_REGEX, res[mass_mailing.id]): + utm_object = mass_mailing.mass_mailing_campaign_id if mass_mailing.mass_mailing_campaign_id else mass_mailing + + vals = {'url': long_url} + + if utm_object.campaign_id: + vals['campaign_id'] = utm_object.campaign_id.id + if utm_object.source_id: + vals['source_id'] = utm_object.source_id.id + if utm_object.medium_id: + vals['medium_id'] = utm_object.medium_id.id + + link_id = website_links.create(cr, uid, vals, context=context) + shorten_url = website_links.browse(cr, uid, link_id, context=context)[0].short_url + + if shorten_url: + res[mass_mailing.id] = res[mass_mailing.id].replace(long_url, shorten_url) + return res + + +class MailMail(models.Model): + _inherit = ['mail.mail'] + + @api.model + def send_get_mail_body(self, mail, partner=None): + """Override to add Statistic_id in shorted urls """ + if mail.mailing_id and mail.body_html: + urls = re.findall(URL_REGEX, mail.body_html) + + for url in urls: + mail.body_html = mail.body_html.replace(url, url + '/m/' + str(mail.statistics_ids[0].id)) + + return super(MailMail, self).send_get_mail_body(mail, partner=partner) diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index c3cd4314fa6..1ec50ebbee3 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -19,7 +19,9 @@ # ############################################################################## -from openerp.osv import osv, fields +import openerp +from openerp.osv import fields, osv +from openerp import models, api, _ class MailMailStats(osv.Model): @@ -63,6 +65,7 @@ class MailMailStats(osv.Model): 'opened': fields.datetime('Opened', help='Date when the email has been opened the first time'), 'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'), 'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'), + 'links_click_ids': fields.one2many('website.links.click', 'mail_stat_id', 'Links click'), } _defaults = { diff --git a/addons/mass_mailing/models/website_links_click.py b/addons/mass_mailing/models/website_links_click.py new file mode 100644 index 00000000000..31e93445ef8 --- /dev/null +++ b/addons/mass_mailing/models/website_links_click.py @@ -0,0 +1,10 @@ +from openerp import fields +from openerp import models + + +class website_links_click(models.Model): + _inherit = "website.links.click" + + mail_stat_id = fields.Many2one('mail.mail.statistics', string='Mail Statistics') + mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') + mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 9f4e1b4937a..010e8f2c12f 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -161,6 +161,12 @@ </field> </record> + <record model="ir.actions.act_window" id="action_view_mass_mailing_statistics"> + <field name="name">Statistics of Clicks</field> + <field name="res_model">website.links.click</field> + <field name="view_mode">graph</field> + </record> + <record model="ir.actions.act_window" id="action_view_mass_mailing_contacts"> <field name="name">Mailing List Subscribers</field> <field name="res_model">mail.mass_mailing.contact</field> @@ -183,7 +189,7 @@ </record> <menuitem name="Contacts" id="menu_email_mass_mailing_contacts" - parent="mass_mailing_campaign" sequence="4" + parent="mass_mailing_campaign" sequence="4" groups="base.group_no_one" action="action_view_mass_mailing_contacts"/> <!-- MASS MAILING LIST --> @@ -337,8 +343,11 @@ type="action" class="oe_stat_button"> <field name="replied_ratio" string="Replied" widget="percentpie"/> </button> - <button name="%(action_view_mail_mail_statistics_mailing)d" - type="action" class="oe_stat_button oe_inline"> + <button name="mass_mailing_statistics_action" + type="object" class="oe_stat_button"> + <field name="clicks_ratio" string="clicks" widget="percentpie"/> + </button> + <button name="%(action_view_mass_mailing_contacts)d" type="action" class="oe_stat_button oe_inline"> <field name="opened_daily" string="Opened Daily" widget="barchart"/> </button> <button name="%(action_view_mail_mail_statistics_mailing)d" @@ -405,7 +414,7 @@ <field name="sent_date" readonly="1"/> </group> <group string="Campaign"> - <field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/> + <field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign" string="Campaign" on_change="onchange_mass_mailing_campaign_id(mass_mailing_campaign_id)"/> <label for="contact_ab_pc"/> <div> <field name="contact_ab_pc" class="oe_inline"/> % @@ -413,6 +422,13 @@ </group> </group> </page> + <page string="Tracking" groups="base.group_no_one"> + <group> + <field name="campaign_id" attrs="{'invisible': True}" /> + <field name="medium_id" /> + <field name="source_id" /> + </group> + </page> </notebook> </sheet> </form> @@ -450,7 +466,8 @@ </div> <div style="display: inline-block; vertical-align: top;"> <strong>Opened</strong> <field name="opened_ratio"/> %<br /> - <strong>Replied</strong> <field name="replied_ratio"/> % + <strong>Replied</strong> <field name="replied_ratio"/> % <br /> + <strong>Clicks</strong> <field name="clicks_ratio"/> % <br /> </div> </div> </div> @@ -551,7 +568,7 @@ <field name="model">mail.mass_mailing.campaign</field> <field name="arch" type="xml"> <search string="Mass Mailing Campaigns"> - <field name="name" string="Campaigns"/> + <field name="name"/> <field name="category_ids"/> <field name="user_id"/> <group expand="0" string="Group By"> @@ -589,7 +606,7 @@ <sheet> <group> <group> - <field name="name"/> + <field name="name" string="Campaign Name" on_change="on_change_campaign_name(name)"/> <field name="user_id"/> <field name="category_ids" widget="many2many_tags"/> </group> @@ -609,22 +626,36 @@ type="action" class="oe_stat_button oe_inline"> <field name="replied_ratio" widget="percentpie" string="Replied"/> </button> + <button name="mass_mailing_statistics_action" + type="object" class="oe_stat_button"> + <field name="clicks_ratio" string="clicks" widget="percentpie"/> + </button> </div> </group> </group> - <strong>Related Mailing(s)</strong> - <field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)"> - <tree> - <field name="name"/> - <field name="sent_date"/> - <field name="state"/> - <field name="delivered"/> - <field name="opened"/> - <field name="replied"/> - <field name="bounced"/> - <button name="action_duplicate" type="object" string="Duplicate"/> - </tree> - </field> + <notebook> + <page string="Related Mailing(s)"> + <field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)"> + <tree> + <field name="name"/> + <field name="sent_date"/> + <field name="state"/> + <field name="delivered"/> + <field name="opened"/> + <field name="replied"/> + <field name="bounced"/> + <button name="action_duplicate" type="object" string="Duplicate"/> + </tree> + </field> + </page> + <page string="Tracking"> + <group> + <field name="campaign_id" attrs="{'invisible': True}"/> + <field name="medium_id" /> + <field name="source_id" /> + </group> + </page> + </notebook> </sheet> </form> </field> @@ -673,7 +704,8 @@ </div> <div style="display: inline-block; vertical-align: top;"> <strong>Opened</strong> <field name="opened_ratio"/> %<br /> - <strong>Replied</strong> <field name="replied_ratio"/> % + <strong>Replied</strong> <field name="replied_ratio"/> % <br /> + <strong>Clicks</strong> <field name="clicks_ratio"/> % <br /> </div> </div> </div> diff --git a/addons/sale_crm/sale_crm.py b/addons/sale_crm/sale_crm.py index ef31fd87a5b..bb777abaa2b 100644 --- a/addons/sale_crm/sale_crm.py +++ b/addons/sale_crm/sale_crm.py @@ -23,7 +23,7 @@ from openerp.osv import osv, fields class sale_order(osv.osv): _name = "sale.order" - _inherit = ['sale.order', 'crm.tracking.mixin'] + _inherit = ['sale.order', 'utm.mixin'] _columns = { 'tag_ids': fields.many2many('crm.lead.tag', 'sale_order_tag_rel', 'order_id', 'tag_id', 'Tags'), 'opportunity_id': fields.many2one('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]") diff --git a/addons/utm/__init__.py b/addons/utm/__init__.py new file mode 100644 index 00000000000..0f7cb6b2272 --- /dev/null +++ b/addons/utm/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import models
\ No newline at end of file diff --git a/addons/utm/__openerp__.py b/addons/utm/__openerp__.py new file mode 100644 index 00000000000..36c96eebbd5 --- /dev/null +++ b/addons/utm/__openerp__.py @@ -0,0 +1,16 @@ +{ + 'name': 'UTM Trackers', + 'category': 'Hidden', + 'description': """ +Enable UTM trackers in shared links. +===================================================== + """, + 'version': '1.0', + 'depends':['marketing'], + 'data' : [ + 'security/ir.model.access.csv', + 'views/utm.xml', + ], + 'demo': ['utm_demo.xml'], + 'auto_install': True, +} diff --git a/addons/utm/models/__init__.py b/addons/utm/models/__init__.py new file mode 100644 index 00000000000..2ee8f89fad8 --- /dev/null +++ b/addons/utm/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import utm
\ No newline at end of file diff --git a/addons/utm/models/utm.py b/addons/utm/models/utm.py new file mode 100644 index 00000000000..4f4d905795d --- /dev/null +++ b/addons/utm/models/utm.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-today Odoo SA (<http://www.odoo.com>) +# +# 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 openerp.osv import osv, fields +from openerp.http import request + + +class utm_medium(osv.Model): + # OLD crm.case.channel + _name = "utm.medium" + _description = "Channels" + _order = 'name' + _columns = { + 'name': fields.char('Channel Name', required=True), + 'active': fields.boolean('Active'), + } + _defaults = { + 'active': lambda *a: 1, + } + + +class utm_campaign(osv.Model): + # OLD crm.case.resource.type + _name = "utm.campaign" + _description = "Campaign" + _rec_name = "name" + _columns = { + 'name': fields.char('Campaign Name', required=True, translate=True), + } + + +class utm_source(osv.Model): + _name = "utm.source" + _description = "Source" + _rec_name = "name" + _columns = { + 'name': fields.char('Source Name', required=True, translate=True), + } + + +class utm_mixin(osv.AbstractModel): + """Mixin class for objects which can be tracked by marketing. """ + _name = 'utm.mixin' + + _columns = { + 'campaign_id': fields.many2one('utm.campaign', 'Campaign', # old domain ="['|',('team_id','=',team_id),('team_id','=',False)]" + help="This is a name that helps you keep track of your different campaign efforts Ex: Fall_Drive, Christmas_Special"), + 'source_id': fields.many2one('utm.source', 'Source', help="This is the source of the link Ex: Search Engine, another domain, or name of email list"), + 'medium_id': fields.many2one('utm.medium', 'Medium', help="This is the method of delivery. Ex: Postcard, Email, or Banner Ad", oldname='channel_id'), + } + + def tracking_fields(self): + return [('utm_campaign', 'campaign_id'), ('utm_source', 'source_id'), ('utm_medium', 'medium_id')] + + def tracking_get_values(self, cr, uid, vals, context=None): + for key, fname in self.tracking_fields(): + field = self._fields[fname] + value = vals.get(fname) or (request and request.httprequest.cookies.get(key)) # params.get should be always in session by the dispatch from ir_http + if field.type == 'many2one' and isinstance(value, basestring): + # if we receive a string for a many2one, we search/create the id + if value: + Model = self.pool[field.comodel_name] + rel_id = Model.name_search(cr, uid, value, context=context) + if rel_id: + rel_id = rel_id[0][0] + else: + rel_id = Model.create(cr, uid, {'name': value}, context=context) + vals[fname] = rel_id + else: + # Here the code for others cases that many2one + vals[fname] = value + return vals + + def _get_default_track(self, cr, uid, field, context=None): + return self.tracking_get_values(cr, uid, {}, context=context).get(field) + + _defaults = { + 'source_id': lambda self, cr, uid, ctx: self._get_default_track(cr, uid, 'source_id', ctx), + 'campaign_id': lambda self, cr, uid, ctx: self._get_default_track(cr, uid, 'campaign_id', ctx), + 'medium_id': lambda self, cr, uid, ctx: self._get_default_track(cr, uid, 'medium_id', ctx), + }
\ No newline at end of file diff --git a/addons/utm/security/ir.model.access.csv b/addons/utm/security/ir.model.access.csv new file mode 100644 index 00000000000..126f6ca10a6 --- /dev/null +++ b/addons/utm/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_utm_campaign,access_utm_campaign,model_utm_campaign,,1,1,1,1 +access_utm_medium,access_utm_medium,model_utm_medium,,1,1,1,1 +access_utm_source,access_utm_source,model_utm_source,,1,1,1,1
\ No newline at end of file diff --git a/addons/utm/utm_demo.xml b/addons/utm/utm_demo.xml new file mode 100644 index 00000000000..4e021490d0e --- /dev/null +++ b/addons/utm/utm_demo.xml @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<openerp> + <data> + <!-- UTM Campain --> + <record model="utm.campaign" id="utm_campaign_fall_drive"> + <field name="name">Fall Drive</field> + </record> + <record model="utm.campaign" id="utm_campaign_christmas_special"> + <field name="name">Christmas Special</field> + </record> + <record model="utm.campaign" id="utm_campaign_email_campaign_services"> + <field name="name">Email Campaign - Services</field> + </record> + <record model="utm.campaign" id="utm_campaign_email_campaign_products"> + <field name="name">Email Campaign - Products</field> + </record> + + <!-- UTM Source --> + <record model="utm.source" id="utm_source_search_engine"> + <field name="name">Search engine</field> + </record> + <record model="utm.source" id="utm_source_mailing"> + <field name="name">Mailing Partner</field> + </record> + <record model="utm.source" id="utm_source_newsletter"> + <field name="name">Newsletter</field> + </record> + <record model="utm.source" id="utm_source_facebook"> + <field name="name">Facebook</field> + </record> + <record model="utm.source" id="utm_source_twitter"> + <field name="name">Twitter</field> + </record> + + <!-- UTM Medium --> + <record model="utm.medium" id="utm_medium_website"> + <field name="name">Website</field> + </record> + <record model="utm.medium" id="utm_medium_phone"> + <field name="name">Phone</field> + </record> + <record model="utm.medium" id="utm_medium_direct"> + <field name="name">Direct</field> + </record> + <record model="utm.medium" id="utm_medium_email"> + <field name="name">Email</field> + </record> + <record model="utm.medium" id="utm_medium_banner"> + <field name="name">Banner Ads</field> + </record> + <record model="utm.medium" id="utm_medium_twitter"> + <field name="name">Twitter Ads</field> + </record> + <record model="utm.medium" id="utm_medium_facebook"> + <field name="name">Facebook Ads</field> + </record> + <record model="utm.medium" id="utm_medium_television"> + <field name="name">Television</field> + </record> + <record model="utm.medium" id="utm_medium_google_adwords"> + <field name="name">Google Adwords</field> + </record> + </data> +</openerp>
\ No newline at end of file diff --git a/addons/utm/views/utm.xml b/addons/utm/views/utm.xml new file mode 100644 index 00000000000..1147af4b5e3 --- /dev/null +++ b/addons/utm/views/utm.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + <menuitem name="UTMs" id="marketing_utm" + parent="base.marketing_menu" sequence="99"/> + + <!-- utm.campaign --> + <record model="ir.ui.view" id="utm_campaign_tree"> + <field name="name">utm.campaign.tree</field> + <field name="model">utm.campaign</field> + <field name="arch" type="xml"> + <tree string="Campaign"> + <field name="name"/> + </tree> + </field> + </record> + + <record model="ir.ui.view" id="utm_campaign_form"> + <field name="name">utm.campaign.form</field> + <field name="model">utm.campaign</field> + <field name="arch" type="xml"> + <form string="Campaign"> + <group> + <field name="name"/> + </group> + </form> + </field> + </record> + + <record id="utm_campaign_act" model="ir.actions.act_window"> + <field name="name">Campaigns</field> + <field name="res_model">utm.campaign</field> + <field name="view_type">form</field> + <field name="view_id" ref="utm_campaign_tree"/> + </record> + + <menuitem action="utm_campaign_act" id="menu_utm_campaign_act" sequence="4" + groups="base.group_no_one" parent="marketing_utm"/> + + <!-- utm.medium --> + <record id="utm_medium_view_tree" model="ir.ui.view"> + <field name="name">utm.medium.tree</field> + <field name="model">utm.medium</field> + <field name="arch" type="xml"> + <tree string="Medium"> + <field name="name"/> + <field name="active"/> + </tree> + </field> + </record> + <record id="utm_medium_view_form" model="ir.ui.view"> + <field name="name">utm.medium.form</field> + <field name="model">utm.medium</field> + <field name="arch" type="xml"> + <form string="Channel"> + <group> + <field name="name"/> + <field name="active"/> + </group> + </form> + </field> + </record> + <record id="utm_medium_action" model="ir.actions.act_window"> + <field name="name">Medium</field> + <field name="res_model">utm.medium</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click to define a new medium. + </p> + </field> + </record> + + <menuitem action="utm_medium_action" id="menu_utm_medium" parent="marketing_utm" sequence="4" groups="base.group_no_one"/> + + <!-- utm.source --> + <record id="utm_source_view_tree" model="ir.ui.view"> + <field name="name">utm.source.tree</field> + <field name="model">utm.source</field> + <field name="arch" type="xml"> + <tree string="Medium"> + <field name="name"/> + </tree> + </field> + </record> + <record id="utm_source_view_form" model="ir.ui.view"> + <field name="name">utm.source.form</field> + <field name="model">utm.source</field> + <field name="arch" type="xml"> + <form string="Channel"> + <group> + <field name="name"/> + </group> + </form> + </field> + </record> + <record id="utm_source_action" model="ir.actions.act_window"> + <field name="name">Sources</field> + <field name="res_model">utm.source</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click to define a new source. + </p> + </field> + </record> + + <menuitem action="utm_source_action" id="menu_utm_source" parent="marketing_utm" sequence="4" groups="base.group_no_one"/> + </data> +</openerp>
\ No newline at end of file diff --git a/addons/website/static/src/js/website.contentMenu.js b/addons/website/static/src/js/website.contentMenu.js index aed70698c6c..bdb6d63c8db 100644 --- a/addons/website/static/src/js/website.contentMenu.js +++ b/addons/website/static/src/js/website.contentMenu.js @@ -9,7 +9,7 @@ website.EditorBarContent = openerp.Widget.extend({ start: function() { var self = this; - self.$el.on('click', 'a', function(ev) { + self.$el.on('click', 'a[data-action]', function(ev) { ev.preventDefault(); var $content_item = $(this); self[$content_item.data('action')](); diff --git a/addons/website_links/__init__.py b/addons/website_links/__init__.py new file mode 100644 index 00000000000..12ce74572a7 --- /dev/null +++ b/addons/website_links/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +import models +import controller
\ No newline at end of file diff --git a/addons/website_links/__openerp__.py b/addons/website_links/__openerp__.py new file mode 100644 index 00000000000..fd195a0eda1 --- /dev/null +++ b/addons/website_links/__openerp__.py @@ -0,0 +1,19 @@ +{ + 'name': 'Link Tracker', + 'category': 'Hidden', + 'description': """ +Create short and trackable URLs. +===================================================== + + """, + 'version': '1.0', + 'depends':['website','marketing', 'utm'], + 'data' : [ + 'views/website_links.xml', + 'views/website_links_template.xml', + 'views/website_links_graphs.xml', + 'security/ir.model.access.csv', + ], + 'qweb': ['static/src/xml/*.xml'], + 'auto_install': True, +} diff --git a/addons/website_links/controller/__init__.py b/addons/website_links/controller/__init__.py new file mode 100644 index 00000000000..a84d81a791b --- /dev/null +++ b/addons/website_links/controller/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import main diff --git a/addons/website_links/controller/main.py b/addons/website_links/controller/main.py new file mode 100644 index 00000000000..46da968a5c4 --- /dev/null +++ b/addons/website_links/controller/main.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import werkzeug + +from openerp.addons.web import http +from openerp.http import request + + +class Website_Url(http.Controller): + @http.route('/website_links/new', type='json', auth='user', methods=['POST']) + def create_shorten_url(self, **post): + if 'url' not in post or post['url'] == '': + return {'error': 'empty_url'} + return request.env['website.links'].create(post).read() + + @http.route('/r', type='http', auth='user', website=True) + def shorten_url(self, **post): + return request.website.render("website_links.page_shorten_url", post) + + @http.route('/website_links/add_code', type='json', auth='user') + def add_code(self, **post): + link_id = request.env['website.links.code'].search([('code', '=', post['init_code'])], limit=1).link_id.id + new_code = request.env['website.links.code'].search_count([('code', '=', post['new_code']), ('link_id', '=', link_id)]) + if new_code > 0: + return new_code.read() + else: + return request.env['website.links.code'].create({'code': post['new_code'], 'link_id': link_id})[0].read() + + @http.route('/website_links/recent_links', type='json', auth='user') + def recent_links(self, **post): + return request.env['website.links'].recent_links(post['filter'], post['limit']) + + @http.route('/r/<string:code>+', type='http', auth="user", website=True) + def statistics_shorten_url(self, code, **post): + code = request.env['website.links.code'].search([('code', '=', code)], limit=1) + + if code: + return request.website.render("website_links.graphs", code.link_id.read()[0]) + else: + return werkzeug.utils.redirect('', 301) + + @http.route('/r/<string:code>', type='http', auth='none', website=True) + def full_url_redirect(self, code, **post): + request.env['website.links.click'].add_click(code, request.httprequest.remote_addr, request.session['geoip'].get('country_code'), stat_id=False) + redirect_url = request.env['website.links'].get_url_from_code(code) + return werkzeug.utils.redirect(redirect_url or '', 301) diff --git a/addons/website_links/models/__init__.py b/addons/website_links/models/__init__.py new file mode 100644 index 00000000000..b49461a2eb5 --- /dev/null +++ b/addons/website_links/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import website_links
\ No newline at end of file diff --git a/addons/website_links/models/website_links.py b/addons/website_links/models/website_links.py new file mode 100644 index 00000000000..6f4f376d413 --- /dev/null +++ b/addons/website_links/models/website_links.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +import datetime +import random +import string + +from lxml.html import parse +from urllib import urlencode +from urllib2 import urlopen +from urlparse import urljoin +from urlparse import urlparse + +from openerp import models, fields, api, _ + + +def VALIDATE_URL(url): + if urlparse(url).scheme not in ('http', 'https', 'ftp', 'ftps'): + return 'http://' + url + + return url + + +class website_links(models.Model): + """website_links allow users to wrap any URL into a short and trackable URL + via a frontend and backend interface. website_links counts clicks on each tracked link. + This module is also used by mass_mailing, where each link in mail_mail html_body are converted into + a trackable link to get the click-through rate of each mass_mailing.""" + _name = "website.links" + _rec_name = "short_url" + + _inherit = ['utm.mixin'] + + url = fields.Char(string='Target URL', required=True) + count = fields.Integer(string='Number of Clicks', compute='_compute_count', store=True) + short_url = fields.Char(string='Tracked URL', compute='_compute_short_url') + link_click_ids = fields.One2many('website.links.click', 'link_id', string='Clicks') + title = fields.Char(string='Page Title', store=True) + favicon = fields.Char(string='Favicon', compute='_compute_favicon', store=True) + link_code_ids = fields.One2many('website.links.code', 'link_id', string='Codes') + code = fields.Char(string='Short URL code', compute='_compute_code') + redirected_url = fields.Char(string='Redirected URL', compute='_compute_redirected_url') + short_url_host = fields.Char(string='Host of the short URL', compute='_compute_short_url_host') + icon_src = fields.Char(string='Favicon Source', compute='_compute_icon_src') + + @api.one + @api.depends('link_click_ids.link_id') + def _compute_count(self): + self.count = len(self.link_click_ids) + + @api.one + @api.depends('code') + def _compute_short_url(self): + base_url = self.env['ir.config_parameter'].get_param('web.base.url') + self.short_url = urljoin(base_url, '/r/%(code)s' % {'code': self.code}) + + @api.one + def _compute_short_url_host(self): + self.short_url_host = self.env['ir.config_parameter'].get_param('web.base.url') + '/r/' + + @api.one + def _compute_code(self): + record = self.env['website.links.code'].search([('link_id', '=', self.id)], limit=1, order='id DESC') + self.code = record.code + + @api.one + @api.depends('favicon') + def _compute_icon_src(self): + self.icon_src = 'data:image/png;base64,' + self.favicon + + @api.one + @api.depends('url') + def _compute_redirected_url(self): + parsed = urlparse(self.url) + + utms = {} + for key, field in self.env['utm.mixin'].tracking_fields(): + attr = getattr(self, field).name + if attr: + utms[key] = attr + + self.redirected_url = '%s://%s%s?%s%s#%s' % (parsed.scheme, parsed.netloc, parsed.path, urlencode(utms), parsed.query, parsed.fragment) + + @api.model + @api.depends('url') + def _get_title_from_url(self, url): + try: + page = urlopen(url, timeout=5) + p = parse(page) + title = p.find('.//title').text + except: + title = url + + return title + + @api.one + @api.depends('url') + def _compute_favicon(self): + try: + icon = urlopen('http://www.google.com/s2/favicons?domain=' + self.url, timeout=5).read() + icon_base64 = icon.encode('base64').replace("\n", "") + except: + icon_base64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAACiElEQVQ4EaVTzU8TURCf2tJuS7tQtlRb6UKBIkQwkRRSEzkQgyEc6lkOKgcOph78Y+CgjXjDs2i44FXY9AMTlQRUELZapVlouy3d7kKtb0Zr0MSLTvL2zb75eL838xtTvV6H/xELBptMJojeXLCXyobnyog4YhzXYvmCFi6qVSfaeRdXdrfaU1areV5KykmX06rcvzumjY/1ggkR3Jh+bNf1mr8v1D5bLuvR3qDgFbvbBJYIrE1mCIoCrKxsHuzK+Rzvsi29+6DEbTZz9unijEYI8ObBgXOzlcrx9OAlXyDYKUCzwwrDQx1wVDGg089Dt+gR3mxmhcUnaWeoxwMbm/vzDFzmDEKMMNhquRqduT1KwXiGt0vre6iSeAUHNDE0d26NBtAXY9BACQyjFusKuL2Ry+IPb/Y9ZglwuVscdHaknUChqLF/O4jn3V5dP4mhgRJgwSYm+gV0Oi3XrvYB30yvhGa7BS70eGFHPoTJyQHhMK+F0ZesRVVznvXw5Ixv7/C10moEo6OZXbWvlFAF9FVZDOqEABUMRIkMd8GnLwVWg9/RkJF9sA4oDfYQAuzzjqzwvnaRUFxn/X2ZlmGLXAE7AL52B4xHgqAUqrC1nSNuoJkQtLkdqReszz/9aRvq90NOKdOS1nch8TpL555WDp49f3uAMXhACRjD5j4ykuCtf5PP7Fm1b0DIsl/VHGezzP1KwOiZQobFF9YyjSRYQETRENSlVzI8iK9mWlzckpSSCQHVALmN9Az1euDho9Xo8vKGd2rqooA8yBcrwHgCqYR0kMkWci08t/R+W4ljDCanWTg9TJGwGNaNk3vYZ7VUdeKsYJGFNkfSzjXNrSX20s4/h6kB81/271ghG17l+rPTAAAAAElFTkSuQmCC' + + self.favicon = icon_base64 + + @api.multi + def action_view_statistics(self): + action = self.env['ir.actions.act_window'].for_xml_id('website_links', 'action_view_click_statistics') + action['domain'] = [('link_id', '=', self.id)] + return action + + @api.multi + def action_visit_page(self): + return { + 'name': _("Visit Webpage"), + 'type': 'ir.actions.act_url', + 'url': self.url, + 'target': 'new', + } + + @api.model + def recent_links(self, filter, limit): + if filter == 'newest': + return self.search_read([], order='create_date DESC', limit=limit) + elif filter == 'most-clicked': + return self.search_read([('count', '!=', 0)], order='count DESC', limit=limit) + elif filter == 'recently-used': + return self.search_read([('count', '!=', 0)], order='write_date DESC', limit=limit) + else: + return {'Error': "This filter doesn't exist."} + + @api.model + def create(self, vals): + if 'url' not in vals: + raise ValueError('URL field required') + else: + vals['url'] = VALIDATE_URL(vals['url']) + + search_domain = [('url', '=', vals['url'])] + + if 'campaign_id' in vals: + search_domain.append(('campaign_id', '=', vals['campaign_id'])) + if 'channel_id' in vals: + search_domain.append(('channel_id', '=', vals['channel_id'])) + if 'source_id' in vals: + search_domain.append(('source_id', '=', vals['source_id'])) + + result = self.search(search_domain, limit=1) + + if result: + return result + + if not vals.get('title'): + vals['title'] = self._get_title_from_url(vals['url']) + + link = super(website_links, self).create(vals) + + code = self.env['website.links.code'].get_random_code_string() + self.env['website.links.code'].create({'code': code, 'link_id': link.id}) + + return link + + @api.model + def get_url_from_code(self, code, context=None): + code_rec = self.env['website.links.code'].sudo().search([('code', '=', code)]) + + if not code_rec: + return None + + return code_rec.link_id.redirected_url + + sql_constraints = [ + ('url_utms_uniq', 'unique (url, campaign_id, medium_id, source_id)', 'The URL and the UTM combination must be unique') + ] + + +class website_links_code(models.Model): + _name = "website.links.code" + + code = fields.Char(string='Short URL Code', store=True) + link_id = fields.Many2one('website.links', 'Link', required=True, ondelete='cascade') + + @api.model + def get_random_code_string(self): + size = 3 + while True: + code_proposition = ''.join(random.choice(string.letters + string.digits) for _ in range(size)) + + if self.search([('code', '=', code_proposition)]): + size += 1 + else: + return code_proposition + + _sql_constraints = [ + ('code', 'unique( code )', 'Code must be unique.') + ] + + +class website_links_click(models.Model): + _name = "website.links.click" + _rec_name = "link_id" + + click_date = fields.Date(string='Create Date') + link_id = fields.Many2one('website.links', 'Link', required=True, ondelete='cascade') + ip = fields.Char(string='Internet Protocol') + country_id = fields.Many2one('res.country', 'Country') + + @api.model + def add_click(self, code, ip, country_code, stat_id=False): + self = self.sudo() + code_rec = self.env['website.links.code'].search([('code', '=', code)]) + + if not code_rec: + return None + + again = self.search_count([('link_id', '=', code_rec.link_id.id), ('ip', '=', ip)]) + + if not again: + country_record = self.env['res.country'].search([('code', '=', country_code)], limit=1) + + vals = { + 'link_id': code_rec.link_id.id, + 'create_date': datetime.date.today(), + 'ip': ip, + 'country_id': country_record.id, + 'mail_stat_id': stat_id + } + + if stat_id: + mail_stat = self.env['mail.mail.statistics'].search([('id', '=', stat_id)]) + + if mail_stat.mass_mailing_campaign_id: + vals['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id + + if mail_stat.mass_mailing_id: + vals['mass_mailing_id'] = mail_stat.mass_mailing_id.id + + self.create(vals) diff --git a/addons/website_links/security/ir.model.access.csv b/addons/website_links/security/ir.model.access.csv new file mode 100644 index 00000000000..a734f942c5f --- /dev/null +++ b/addons/website_links/security/ir.model.access.csv @@ -0,0 +1,10 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_website_links_user,website.links,model_website_links,base.group_user,1,0,0,0 +access_website_links_manager,website.links,model_website_links,base.group_erp_manager,1,1,1,1 +access_website_links_public,website.links,model_website_links,base.group_public,1,0,0,0 +access_website_links_code_user,website.links.code,model_website_links_code,base.group_user,1,0,0,0 +access_website_links_code_manager,website.links.code,model_website_links_code,base.group_erp_manager,1,1,1,1 +access_website_links_code_public,website.links.code,model_website_links_code,base.group_public,1,0,0,0 +access_website_links_click_user,website.links.click,model_website_links_click,base.group_user,1,0,0,0 +access_website_links_click_manager,website.links.click,model_website_links_click,base.group_erp_manager,1,1,1,1 +access_website_links_click_public,website.links.click,model_website_links_click,base.group_public,1,0,0,0 diff --git a/addons/website_links/static/src/css/website_links.css b/addons/website_links/static/src/css/website_links.css new file mode 100644 index 00000000000..3664ecb31ac --- /dev/null +++ b/addons/website_links/static/src/css/website_links.css @@ -0,0 +1,57 @@ +.no-link-style { + color: black; + text-decoration: none; +} + +.required-form-control { + background-color: #CECDFF; +} + +#filters li a, +#filters li.active a, +#filters li.active a.active, +#filters li.active a:hover, +#filters li.active a:focus, +#filters li a:hover { + padding: 0px 5px; + border-radius: 0px; + border: 0px; + border-color: transparent; + border-right: 1px solid #999; + padding-bottom: 0; + background-color: #FFFFFF; +} + +#filters li.active a { + color: #999; +} + +#filters li:last-child a, +#filters li:last-child a:hover, +#filters li:last-child a:focus { + border-right: 0px; +} + +.nav-tabs-inline { + float: left; + display: inline; + font-size: 14px; + border-color: transparent; +} + +#o_website_links_recent_links { + min-height:30em; +} + +.o_website_links_code_error { + display:none; + color:red; + font-weight:bold; +} + +.truncate_text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 500px; +}
\ No newline at end of file diff --git a/addons/website_links/static/src/js/ZeroClipboard.js b/addons/website_links/static/src/js/ZeroClipboard.js new file mode 100755 index 00000000000..5778b0cf978 --- /dev/null +++ b/addons/website_links/static/src/js/ZeroClipboard.js @@ -0,0 +1,2314 @@ +/*! + * ZeroClipboard + * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. + * Copyright (c) 2014 Jon Rohan, James M. Greene + * Licensed MIT + * http://zeroclipboard.org/ + * v2.1.6 + */ +(function(window, undefined) { + "use strict"; + /** + * Store references to critically important global functions that may be + * overridden on certain web pages. + */ + var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _round = _window.Math.round, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() { + var unwrapper = function(el) { + return el; + }; + if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") { + try { + var div = _document.createElement("div"); + var unwrappedDiv = _window.unwrap(div); + if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) { + unwrapper = _window.unwrap; + } + } catch (e) {} + } + return unwrapper; + }(); + /** + * Convert an `arguments` object into an Array. + * + * @returns The arguments as an Array + * @private + */ + var _args = function(argumentsObj) { + return _slice.call(argumentsObj, 0); + }; + /** + * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`. + * + * @returns The target object, augmented + * @private + */ + var _extend = function() { + var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {}; + for (i = 1, len = args.length; i < len; i++) { + if ((arg = args[i]) != null) { + for (prop in arg) { + if (_hasOwn.call(arg, prop)) { + src = target[prop]; + copy = arg[prop]; + if (target !== copy && copy !== undefined) { + target[prop] = copy; + } + } + } + } + } + return target; + }; + /** + * Return a deep copy of the source object or array. + * + * @returns Object or Array + * @private + */ + var _deepCopy = function(source) { + var copy, i, len, prop; + if (typeof source !== "object" || source == null) { + copy = source; + } else if (typeof source.length === "number") { + copy = []; + for (i = 0, len = source.length; i < len; i++) { + if (_hasOwn.call(source, i)) { + copy[i] = _deepCopy(source[i]); + } + } + } else { + copy = {}; + for (prop in source) { + if (_hasOwn.call(source, prop)) { + copy[prop] = _deepCopy(source[prop]); + } + } + } + return copy; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep. + * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to + * be kept. + * + * @returns A new filtered object. + * @private + */ + var _pick = function(obj, keys) { + var newObj = {}; + for (var i = 0, len = keys.length; i < len; i++) { + if (keys[i] in obj) { + newObj[keys[i]] = obj[keys[i]]; + } + } + return newObj; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit. + * The inverse of `_pick`. + * + * @returns A new filtered object. + * @private + */ + var _omit = function(obj, keys) { + var newObj = {}; + for (var prop in obj) { + if (keys.indexOf(prop) === -1) { + newObj[prop] = obj[prop]; + } + } + return newObj; + }; + /** + * Remove all owned, enumerable properties from an object. + * + * @returns The original object without its owned, enumerable properties. + * @private + */ + var _deleteOwnProperties = function(obj) { + if (obj) { + for (var prop in obj) { + if (_hasOwn.call(obj, prop)) { + delete obj[prop]; + } + } + } + return obj; + }; + /** + * Determine if an element is contained within another element. + * + * @returns Boolean + * @private + */ + var _containedBy = function(el, ancestorEl) { + if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) { + do { + if (el === ancestorEl) { + return true; + } + el = el.parentNode; + } while (el); + } + return false; + }; + /** + * Get the URL path's parent directory. + * + * @returns String or `undefined` + * @private + */ + var _getDirPathOfUrl = function(url) { + var dir; + if (typeof url === "string" && url) { + dir = url.split("#")[0].split("?")[0]; + dir = url.slice(0, url.lastIndexOf("/") + 1); + } + return dir; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromErrorStack = function(stack) { + var url, matches; + if (typeof stack === "string" && stack) { + matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } else { + matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } + } + } + return url; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromError = function() { + var url, err; + try { + throw new _Error(); + } catch (e) { + err = e; + } + if (err) { + url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack); + } + return url; + }; + /** + * Get the current script's URL. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrl = function() { + var jsPath, scripts, i; + if (_document.currentScript && (jsPath = _document.currentScript.src)) { + return jsPath; + } + scripts = _document.getElementsByTagName("script"); + if (scripts.length === 1) { + return scripts[0].src || undefined; + } + if ("readyState" in scripts[0]) { + for (i = scripts.length; i--; ) { + if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { + return jsPath; + } + } + } + if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { + return jsPath; + } + if (jsPath = _getCurrentScriptUrlFromError()) { + return jsPath; + } + return undefined; + }; + /** + * Get the unanimous parent directory of ALL script tags. + * If any script tags are either (a) inline or (b) from differing parent + * directories, this method must return `undefined`. + * + * @returns String or `undefined` + * @private + */ + var _getUnanimousScriptParentDir = function() { + var i, jsDir, jsPath, scripts = _document.getElementsByTagName("script"); + for (i = scripts.length; i--; ) { + if (!(jsPath = scripts[i].src)) { + jsDir = null; + break; + } + jsPath = _getDirPathOfUrl(jsPath); + if (jsDir == null) { + jsDir = jsPath; + } else if (jsDir !== jsPath) { + jsDir = null; + break; + } + } + return jsDir || undefined; + }; + /** + * Get the presumed location of the "ZeroClipboard.swf" file, based on the location + * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). + * + * @returns String + * @private + */ + var _getDefaultSwfPath = function() { + var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || ""; + return jsDir + "ZeroClipboard.swf"; + }; + /** + * Keep track of the state of the Flash object. + * @private + */ + var _flashState = { + bridge: null, + version: "0.0.0", + pluginType: "unknown", + disabled: null, + outdated: null, + unavailable: null, + deactivated: null, + overdue: null, + ready: null + }; + /** + * The minimum Flash Player version required to use ZeroClipboard completely. + * @readonly + * @private + */ + var _minimumFlashVersion = "11.0.0"; + /** + * Keep track of all event listener registrations. + * @private + */ + var _handlers = {}; + /** + * Keep track of the currently activated element. + * @private + */ + var _currentElement; + /** + * Keep track of the element that was activated when a `copy` process started. + * @private + */ + var _copyTarget; + /** + * Keep track of data for the pending clipboard transaction. + * @private + */ + var _clipData = {}; + /** + * Keep track of data formats for the pending clipboard transaction. + * @private + */ + var _clipDataFormatMap = null; + /** + * The `message` store for events + * @private + */ + var _eventMessages = { + ready: "Flash communication is established", + error: { + "flash-disabled": "Flash is disabled or not installed", + "flash-outdated": "Flash is too outdated to support ZeroClipboard", + "flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript", + "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate", + "flash-overdue": "Flash communication was established but NOT within the acceptable time limit" + } + }; + /** + * ZeroClipboard configuration defaults for the Core module. + * @private + */ + var _globalConfig = { + swfPath: _getDefaultSwfPath(), + trustedDomains: window.location.host ? [ window.location.host ] : [], + cacheBust: true, + forceEnhancedClipboard: false, + flashLoadTimeout: 3e4, + autoActivate: true, + bubbleEvents: true, + containerId: "global-zeroclipboard-html-bridge", + containerClass: "global-zeroclipboard-container", + swfObjectId: "global-zeroclipboard-flash-bridge", + hoverClass: "zeroclipboard-is-hover", + activeClass: "zeroclipboard-is-active", + forceHandCursor: false, + title: null, + zIndex: 999999999 + }; + /** + * The underlying implementation of `ZeroClipboard.config`. + * @private + */ + var _config = function(options) { + if (typeof options === "object" && options !== null) { + for (var prop in options) { + if (_hasOwn.call(options, prop)) { + if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) { + _globalConfig[prop] = options[prop]; + } else if (_flashState.bridge == null) { + if (prop === "containerId" || prop === "swfObjectId") { + if (_isValidHtml4Id(options[prop])) { + _globalConfig[prop] = options[prop]; + } else { + throw new Error("The specified `" + prop + "` value is not valid as an HTML4 Element ID"); + } + } else { + _globalConfig[prop] = options[prop]; + } + } + } + } + } + if (typeof options === "string" && options) { + if (_hasOwn.call(_globalConfig, options)) { + return _globalConfig[options]; + } + return; + } + return _deepCopy(_globalConfig); + }; + /** + * The underlying implementation of `ZeroClipboard.state`. + * @private + */ + var _state = function() { + return { + browser: _pick(_navigator, [ "userAgent", "platform", "appName" ]), + flash: _omit(_flashState, [ "bridge" ]), + zeroclipboard: { + version: ZeroClipboard.version, + config: ZeroClipboard.config() + } + }; + }; + /** + * The underlying implementation of `ZeroClipboard.isFlashUnusable`. + * @private + */ + var _isFlashUnusable = function() { + return !!(_flashState.disabled || _flashState.outdated || _flashState.unavailable || _flashState.deactivated); + }; + /** + * The underlying implementation of `ZeroClipboard.on`. + * @private + */ + var _on = function(eventType, listener) { + var i, len, events, added = {}; + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!_handlers[eventType]) { + _handlers[eventType] = []; + } + _handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + ZeroClipboard.emit({ + type: "ready" + }); + } + if (added.error) { + var errorTypes = [ "disabled", "outdated", "unavailable", "deactivated", "overdue" ]; + for (i = 0, len = errorTypes.length; i < len; i++) { + if (_flashState[errorTypes[i]] === true) { + ZeroClipboard.emit({ + type: "error", + name: "flash-" + errorTypes[i] + }); + break; + } + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.off`. + * @private + */ + var _off = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers; + if (arguments.length === 0) { + events = _keys(_handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = _handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.handlers`. + * @private + */ + var _listeners = function(eventType) { + var copy; + if (typeof eventType === "string" && eventType) { + copy = _deepCopy(_handlers[eventType]) || null; + } else { + copy = _deepCopy(_handlers); + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.emit`. + * @private + */ + var _emit = function(event) { + var eventCopy, returnVal, tmp; + event = _createEvent(event); + if (!event) { + return; + } + if (_preprocessEvent(event)) { + return; + } + if (event.type === "ready" && _flashState.overdue === true) { + return ZeroClipboard.emit({ + type: "error", + name: "flash-overdue" + }); + } + eventCopy = _extend({}, event); + _dispatchCallbacks.call(this, eventCopy); + if (event.type === "copy") { + tmp = _mapClipDataToFlash(_clipData); + returnVal = tmp.data; + _clipDataFormatMap = tmp.formatMap; + } + return returnVal; + }; + /** + * The underlying implementation of `ZeroClipboard.create`. + * @private + */ + var _create = function() { + if (typeof _flashState.ready !== "boolean") { + _flashState.ready = false; + } + if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + _setTimeout(function() { + if (typeof _flashState.deactivated !== "boolean") { + _flashState.deactivated = true; + } + if (_flashState.deactivated === true) { + ZeroClipboard.emit({ + type: "error", + name: "flash-deactivated" + }); + } + }, maxWait); + } + _flashState.overdue = false; + _embedSwf(); + } + }; + /** + * The underlying implementation of `ZeroClipboard.destroy`. + * @private + */ + var _destroy = function() { + ZeroClipboard.clearData(); + ZeroClipboard.blur(); + ZeroClipboard.emit("destroy"); + _unembedSwf(); + ZeroClipboard.off(); + }; + /** + * The underlying implementation of `ZeroClipboard.setData`. + * @private + */ + var _setData = function(format, data) { + var dataObj; + if (typeof format === "object" && format && typeof data === "undefined") { + dataObj = format; + ZeroClipboard.clearData(); + } else if (typeof format === "string" && format) { + dataObj = {}; + dataObj[format] = data; + } else { + return; + } + for (var dataFormat in dataObj) { + if (typeof dataFormat === "string" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === "string" && dataObj[dataFormat]) { + _clipData[dataFormat] = dataObj[dataFormat]; + } + } + }; + /** + * The underlying implementation of `ZeroClipboard.clearData`. + * @private + */ + var _clearData = function(format) { + if (typeof format === "undefined") { + _deleteOwnProperties(_clipData); + _clipDataFormatMap = null; + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + delete _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.getData`. + * @private + */ + var _getData = function(format) { + if (typeof format === "undefined") { + return _deepCopy(_clipData); + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + return _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`. + * @private + */ + var _focus = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.activeClass); + if (_currentElement !== element) { + _removeClass(_currentElement, _globalConfig.hoverClass); + } + } + _currentElement = element; + _addClass(element, _globalConfig.hoverClass); + var newTitle = element.getAttribute("title") || _globalConfig.title; + if (typeof newTitle === "string" && newTitle) { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.setAttribute("title", newTitle); + } + } + var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, "cursor") === "pointer"; + _setHandCursor(useHandCursor); + _reposition(); + }; + /** + * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`. + * @private + */ + var _blur = function() { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.removeAttribute("title"); + htmlBridge.style.left = "0px"; + htmlBridge.style.top = "-9999px"; + htmlBridge.style.width = "1px"; + htmlBridge.style.top = "1px"; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.hoverClass); + _removeClass(_currentElement, _globalConfig.activeClass); + _currentElement = null; + } + }; + /** + * The underlying implementation of `ZeroClipboard.activeElement`. + * @private + */ + var _activeElement = function() { + return _currentElement || null; + }; + /** + * Check if a value is a valid HTML4 `ID` or `Name` token. + * @private + */ + var _isValidHtml4Id = function(id) { + return typeof id === "string" && id && /^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(id); + }; + /** + * Create or update an `event` object, based on the `eventType`. + * @private + */ + var _createEvent = function(event) { + var eventType; + if (typeof event === "string" && event) { + eventType = event; + event = {}; + } else if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + eventType = event.type; + } + if (!eventType) { + return; + } + if (!event.target && /^(copy|aftercopy|_click)$/.test(eventType.toLowerCase())) { + event.target = _copyTarget; + } + _extend(event, { + type: eventType.toLowerCase(), + target: event.target || _currentElement || null, + relatedTarget: event.relatedTarget || null, + currentTarget: _flashState && _flashState.bridge || null, + timeStamp: event.timeStamp || _now() || null + }); + var msg = _eventMessages[event.type]; + if (event.type === "error" && event.name && msg) { + msg = msg[event.name]; + } + if (msg) { + event.message = msg; + } + if (event.type === "ready") { + _extend(event, { + target: null, + version: _flashState.version + }); + } + if (event.type === "error") { + if (/^flash-(disabled|outdated|unavailable|deactivated|overdue)$/.test(event.name)) { + _extend(event, { + target: null, + minimumVersion: _minimumFlashVersion + }); + } + if (/^flash-(outdated|unavailable|deactivated|overdue)$/.test(event.name)) { + _extend(event, { + version: _flashState.version + }); + } + } + if (event.type === "copy") { + event.clipboardData = { + setData: ZeroClipboard.setData, + clearData: ZeroClipboard.clearData + }; + } + if (event.type === "aftercopy") { + event = _mapClipResultsFromFlash(event, _clipDataFormatMap); + } + if (event.target && !event.relatedTarget) { + event.relatedTarget = _getRelatedTarget(event.target); + } + event = _addMouseData(event); + return event; + }; + /** + * Get a relatedTarget from the target's `data-clipboard-target` attribute + * @private + */ + var _getRelatedTarget = function(targetEl) { + var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target"); + return relatedTargetId ? _document.getElementById(relatedTargetId) : null; + }; + /** + * Add element and position data to `MouseEvent` instances + * @private + */ + var _addMouseData = function(event) { + if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + var srcElement = event.target; + var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; + var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; + var pos = _getDOMObjectPosition(srcElement); + var screenLeft = _window.screenLeft || _window.screenX || 0; + var screenTop = _window.screenTop || _window.screenY || 0; + var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; + var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; + var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); + var pageY = pos.top + (typeof event._stageY === "number" ? event._stageY : 0); + var clientX = pageX - scrollLeft; + var clientY = pageY - scrollTop; + var screenX = screenLeft + clientX; + var screenY = screenTop + clientY; + var moveX = typeof event.movementX === "number" ? event.movementX : 0; + var moveY = typeof event.movementY === "number" ? event.movementY : 0; + delete event._stageX; + delete event._stageY; + _extend(event, { + srcElement: srcElement, + fromElement: fromElement, + toElement: toElement, + screenX: screenX, + screenY: screenY, + pageX: pageX, + pageY: pageY, + clientX: clientX, + clientY: clientY, + x: clientX, + y: clientY, + movementX: moveX, + movementY: moveY, + offsetX: 0, + offsetY: 0, + layerX: 0, + layerY: 0 + }); + } + return event; + }; + /** + * Determine if an event's registered handlers should be execute synchronously or asynchronously. + * + * @returns {boolean} + * @private + */ + var _shouldPerformAsync = function(event) { + var eventType = event && typeof event.type === "string" && event.type || ""; + return !/^(?:(?:before)?copy|destroy)$/.test(eventType); + }; + /** + * Control if a callback should be executed asynchronously or not. + * + * @returns `undefined` + * @private + */ + var _dispatchCallback = function(func, context, args, async) { + if (async) { + _setTimeout(function() { + func.apply(context, args); + }, 0); + } else { + func.apply(context, args); + } + }; + /** + * Handle the actual dispatching of events to client instances. + * + * @returns `undefined` + * @private + */ + var _dispatchCallbacks = function(event) { + if (!(typeof event === "object" && event && event.type)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = _handlers["*"] || []; + var specificTypeHandlers = _handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + return this; + }; + /** + * Preprocess any special behaviors, reactions, or state changes after receiving this event. + * Executes only once per event emitted, NOT once per client. + * @private + */ + var _preprocessEvent = function(event) { + var element = event.target || _currentElement || null; + var sourceIsSwf = event._source === "swf"; + delete event._source; + var flashErrorNames = [ "flash-disabled", "flash-outdated", "flash-unavailable", "flash-deactivated", "flash-overdue" ]; + switch (event.type) { + case "error": + if (flashErrorNames.indexOf(event.name) !== -1) { + _extend(_flashState, { + disabled: event.name === "flash-disabled", + outdated: event.name === "flash-outdated", + unavailable: event.name === "flash-unavailable", + deactivated: event.name === "flash-deactivated", + overdue: event.name === "flash-overdue", + ready: false + }); + } + break; + + case "ready": + var wasDeactivated = _flashState.deactivated === true; + _extend(_flashState, { + disabled: false, + outdated: false, + unavailable: false, + deactivated: false, + overdue: wasDeactivated, + ready: !wasDeactivated + }); + break; + + case "beforecopy": + _copyTarget = element; + break; + + case "copy": + var textContent, htmlContent, targetEl = event.relatedTarget; + if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + if (htmlContent !== textContent) { + event.clipboardData.setData("text/html", htmlContent); + } + } else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + } + break; + + case "aftercopy": + ZeroClipboard.clearData(); + if (element && element !== _safeActiveElement() && element.focus) { + element.focus(); + } + break; + + case "_mouseover": + ZeroClipboard.focus(element); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseenter", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseover" + })); + } + break; + + case "_mouseout": + ZeroClipboard.blur(); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseleave", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseout" + })); + } + break; + + case "_mousedown": + _addClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mouseup": + _removeClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_click": + _copyTarget = null; + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mousemove": + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + } + if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + return true; + } + }; + /** + * Dispatch a synthetic MouseEvent. + * + * @returns `undefined` + * @private + */ + var _fireMouseEvent = function(event) { + if (!(event && typeof event.type === "string" && event)) { + return; + } + var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = { + view: doc.defaultView || _window, + canBubble: true, + cancelable: true, + detail: event.type === "click" ? 1 : 0, + button: typeof event.which === "number" ? event.which - 1 : typeof event.button === "number" ? event.button : doc.createEvent ? 0 : 1 + }, args = _extend(defaults, event); + if (!target) { + return; + } + if (doc.createEvent && target.dispatchEvent) { + args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ]; + e = doc.createEvent("MouseEvents"); + if (e.initMouseEvent) { + e.initMouseEvent.apply(e, args); + e._source = "js"; + target.dispatchEvent(e); + } + } + }; + /** + * Create the HTML bridge element to embed the Flash object into. + * @private + */ + var _createHtmlBridge = function() { + var container = _document.createElement("div"); + container.id = _globalConfig.containerId; + container.className = _globalConfig.containerClass; + container.style.position = "absolute"; + container.style.left = "0px"; + container.style.top = "-9999px"; + container.style.width = "1px"; + container.style.height = "1px"; + container.style.zIndex = "" + _getSafeZIndex(_globalConfig.zIndex); + return container; + }; + /** + * Get the HTML element container that wraps the Flash bridge object/element. + * @private + */ + var _getHtmlBridge = function(flashBridge) { + var htmlBridge = flashBridge && flashBridge.parentNode; + while (htmlBridge && htmlBridge.nodeName === "OBJECT" && htmlBridge.parentNode) { + htmlBridge = htmlBridge.parentNode; + } + return htmlBridge || null; + }; + /** + * Create the SWF object. + * + * @returns The SWF object reference. + * @private + */ + var _embedSwf = function() { + var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); + if (!flashBridge) { + var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); + var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; + var flashvars = _vars(_globalConfig); + var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); + container = _createHtmlBridge(); + var divToBeReplaced = _document.createElement("div"); + container.appendChild(divToBeReplaced); + _document.body.appendChild(container); + var tmpDiv = _document.createElement("div"); + var oldIE = _flashState.pluginType === "activex"; + tmpDiv.innerHTML = '<object id="' + _globalConfig.swfObjectId + '" name="' + _globalConfig.swfObjectId + '" ' + 'width="100%" height="100%" ' + (oldIE ? 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' : 'type="application/x-shockwave-flash" data="' + swfUrl + '"') + ">" + (oldIE ? '<param name="movie" value="' + swfUrl + '"/>' : "") + '<param name="allowScriptAccess" value="' + allowScriptAccess + '"/>' + '<param name="allowNetworking" value="' + allowNetworking + '"/>' + '<param name="menu" value="false"/>' + '<param name="wmode" value="transparent"/>' + '<param name="flashvars" value="' + flashvars + '"/>' + "</object>"; + flashBridge = tmpDiv.firstChild; + tmpDiv = null; + _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; + container.replaceChild(flashBridge, divToBeReplaced); + } + if (!flashBridge) { + flashBridge = _document[_globalConfig.swfObjectId]; + if (flashBridge && (len = flashBridge.length)) { + flashBridge = flashBridge[len - 1]; + } + if (!flashBridge && container) { + flashBridge = container.firstChild; + } + } + _flashState.bridge = flashBridge || null; + return flashBridge; + }; + /** + * Destroy the SWF object. + * @private + */ + var _unembedSwf = function() { + var flashBridge = _flashState.bridge; + if (flashBridge) { + var htmlBridge = _getHtmlBridge(flashBridge); + if (htmlBridge) { + if (_flashState.pluginType === "activex" && "readyState" in flashBridge) { + flashBridge.style.display = "none"; + (function removeSwfFromIE() { + if (flashBridge.readyState === 4) { + for (var prop in flashBridge) { + if (typeof flashBridge[prop] === "function") { + flashBridge[prop] = null; + } + } + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } else { + _setTimeout(removeSwfFromIE, 10); + } + })(); + } else { + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } + } + _flashState.ready = null; + _flashState.bridge = null; + _flashState.deactivated = null; + } + }; + /** + * Map the data format names of the "clipData" to Flash-friendly names. + * + * @returns A new transformed object. + * @private + */ + var _mapClipDataToFlash = function(clipData) { + var newClipData = {}, formatMap = {}; + if (!(typeof clipData === "object" && clipData)) { + return; + } + for (var dataFormat in clipData) { + if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === "string" && clipData[dataFormat]) { + switch (dataFormat.toLowerCase()) { + case "text/plain": + case "text": + case "air:text": + case "flash:text": + newClipData.text = clipData[dataFormat]; + formatMap.text = dataFormat; + break; + + case "text/html": + case "html": + case "air:html": + case "flash:html": + newClipData.html = clipData[dataFormat]; + formatMap.html = dataFormat; + break; + + case "application/rtf": + case "text/rtf": + case "rtf": + case "richtext": + case "air:rtf": + case "flash:rtf": + newClipData.rtf = clipData[dataFormat]; + formatMap.rtf = dataFormat; + break; + + default: + break; + } + } + } + return { + data: newClipData, + formatMap: formatMap + }; + }; + /** + * Map the data format names from Flash-friendly names back to their original "clipData" names (via a format mapping). + * + * @returns A new transformed object. + * @private + */ + var _mapClipResultsFromFlash = function(clipResults, formatMap) { + if (!(typeof clipResults === "object" && clipResults && typeof formatMap === "object" && formatMap)) { + return clipResults; + } + var newResults = {}; + for (var prop in clipResults) { + if (_hasOwn.call(clipResults, prop)) { + if (prop !== "success" && prop !== "data") { + newResults[prop] = clipResults[prop]; + continue; + } + newResults[prop] = {}; + var tmpHash = clipResults[prop]; + for (var dataFormat in tmpHash) { + if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { + newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; + } + } + } + } + return newResults; + }; + /** + * Will look at a path, and will create a "?noCache={time}" or "&noCache={time}" + * query param string to return. Does NOT append that string to the original path. + * This is useful because ExternalInterface often breaks when a Flash SWF is cached. + * + * @returns The `noCache` query param with necessary "?"/"&" prefix. + * @private + */ + var _cacheBust = function(path, options) { + var cacheBust = options == null || options && options.cacheBust === true; + if (cacheBust) { + return (path.indexOf("?") === -1 ? "?" : "&") + "noCache=" + _now(); + } else { + return ""; + } + }; + /** + * Creates a query string for the FlashVars param. + * Does NOT include the cache-busting query param. + * + * @returns FlashVars query string + * @private + */ + var _vars = function(options) { + var i, len, domain, domains, str = "", trustedOriginsExpanded = []; + if (options.trustedDomains) { + if (typeof options.trustedDomains === "string") { + domains = [ options.trustedDomains ]; + } else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) { + domains = options.trustedDomains; + } + } + if (domains && domains.length) { + for (i = 0, len = domains.length; i < len; i++) { + if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === "string") { + domain = _extractDomain(domains[i]); + if (!domain) { + continue; + } + if (domain === "*") { + trustedOriginsExpanded.length = 0; + trustedOriginsExpanded.push(domain); + break; + } + trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, "//" + domain, _window.location.protocol + "//" + domain ]); + } + } + } + if (trustedOriginsExpanded.length) { + str += "trustedOrigins=" + _encodeURIComponent(trustedOriginsExpanded.join(",")); + } + if (options.forceEnhancedClipboard === true) { + str += (str ? "&" : "") + "forceEnhancedClipboard=true"; + } + if (typeof options.swfObjectId === "string" && options.swfObjectId) { + str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); + } + return str; + }; + /** + * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or + * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). + * + * @returns the domain + * @private + */ + var _extractDomain = function(originOrUrl) { + if (originOrUrl == null || originOrUrl === "") { + return null; + } + originOrUrl = originOrUrl.replace(/^\s+|\s+$/g, ""); + if (originOrUrl === "") { + return null; + } + var protocolIndex = originOrUrl.indexOf("//"); + originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2); + var pathIndex = originOrUrl.indexOf("/"); + originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex); + if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === ".swf") { + return null; + } + return originOrUrl || null; + }; + /** + * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`. + * + * @returns The appropriate script access level. + * @private + */ + var _determineScriptAccess = function() { + var _extractAllDomains = function(origins) { + var i, len, tmp, resultsArray = []; + if (typeof origins === "string") { + origins = [ origins ]; + } + if (!(typeof origins === "object" && origins && typeof origins.length === "number")) { + return resultsArray; + } + for (i = 0, len = origins.length; i < len; i++) { + if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) { + if (tmp === "*") { + resultsArray.length = 0; + resultsArray.push("*"); + break; + } + if (resultsArray.indexOf(tmp) === -1) { + resultsArray.push(tmp); + } + } + } + return resultsArray; + }; + return function(currentDomain, configOptions) { + var swfDomain = _extractDomain(configOptions.swfPath); + if (swfDomain === null) { + swfDomain = currentDomain; + } + var trustedDomains = _extractAllDomains(configOptions.trustedDomains); + var len = trustedDomains.length; + if (len > 0) { + if (len === 1 && trustedDomains[0] === "*") { + return "always"; + } + if (trustedDomains.indexOf(currentDomain) !== -1) { + if (len === 1 && currentDomain === swfDomain) { + return "sameDomain"; + } + return "always"; + } + } + return "never"; + }; + }(); + /** + * Get the currently active/focused DOM element. + * + * @returns the currently active/focused element, or `null` + * @private + */ + var _safeActiveElement = function() { + try { + return _document.activeElement; + } catch (err) { + return null; + } + }; + /** + * Add a class to an element, if it doesn't already have it. + * + * @returns The element, with its new class added. + * @private + */ + var _addClass = function(element, value) { + if (!element || element.nodeType !== 1) { + return element; + } + if (element.classList) { + if (!element.classList.contains(value)) { + element.classList.add(value); + } + return element; + } + if (value && typeof value === "string") { + var classNames = (value || "").split(/\s+/); + if (element.nodeType === 1) { + if (!element.className) { + element.className = value; + } else { + var className = " " + element.className + " ", setClass = element.className; + for (var c = 0, cl = classNames.length; c < cl; c++) { + if (className.indexOf(" " + classNames[c] + " ") < 0) { + setClass += " " + classNames[c]; + } + } + element.className = setClass.replace(/^\s+|\s+$/g, ""); + } + } + } + return element; + }; + /** + * Remove a class from an element, if it has it. + * + * @returns The element, with its class removed. + * @private + */ + var _removeClass = function(element, value) { + if (!element || element.nodeType !== 1) { + return element; + } + if (element.classList) { + if (element.classList.contains(value)) { + element.classList.remove(value); + } + return element; + } + if (typeof value === "string" && value) { + var classNames = value.split(/\s+/); + if (element.nodeType === 1 && element.className) { + var className = (" " + element.className + " ").replace(/[\n\t]/g, " "); + for (var c = 0, cl = classNames.length; c < cl; c++) { + className = className.replace(" " + classNames[c] + " ", " "); + } + element.className = className.replace(/^\s+|\s+$/g, ""); + } + } + return element; + }; + /** + * Attempt to interpret the element's CSS styling. If `prop` is `"cursor"`, + * then we assume that it should be a hand ("pointer") cursor if the element + * is an anchor element ("a" tag). + * + * @returns The computed style property. + * @private + */ + var _getStyle = function(el, prop) { + var value = _window.getComputedStyle(el, null).getPropertyValue(prop); + if (prop === "cursor") { + if (!value || value === "auto") { + if (el.nodeName === "A") { + return "pointer"; + } + } + } + return value; + }; + /** + * Get the zoom factor of the browser. Always returns `1.0`, except at + * non-default zoom levels in IE<8 and some older versions of WebKit. + * + * @returns Floating unit percentage of the zoom factor (e.g. 150% = `1.5`). + * @private + */ + var _getZoomFactor = function() { + var rect, physicalWidth, logicalWidth, zoomFactor = 1; + if (typeof _document.body.getBoundingClientRect === "function") { + rect = _document.body.getBoundingClientRect(); + physicalWidth = rect.right - rect.left; + logicalWidth = _document.body.offsetWidth; + zoomFactor = _round(physicalWidth / logicalWidth * 100) / 100; + } + return zoomFactor; + }; + /** + * Get the DOM positioning info of an element. + * + * @returns Object containing the element's position, width, and height. + * @private + */ + var _getDOMObjectPosition = function(obj) { + var info = { + left: 0, + top: 0, + width: 0, + height: 0 + }; + if (obj.getBoundingClientRect) { + var rect = obj.getBoundingClientRect(); + var pageXOffset, pageYOffset, zoomFactor; + if ("pageXOffset" in _window && "pageYOffset" in _window) { + pageXOffset = _window.pageXOffset; + pageYOffset = _window.pageYOffset; + } else { + zoomFactor = _getZoomFactor(); + pageXOffset = _round(_document.documentElement.scrollLeft / zoomFactor); + pageYOffset = _round(_document.documentElement.scrollTop / zoomFactor); + } + var leftBorderWidth = _document.documentElement.clientLeft || 0; + var topBorderWidth = _document.documentElement.clientTop || 0; + info.left = rect.left + pageXOffset - leftBorderWidth; + info.top = rect.top + pageYOffset - topBorderWidth; + info.width = "width" in rect ? rect.width : rect.right - rect.left; + info.height = "height" in rect ? rect.height : rect.bottom - rect.top; + } + return info; + }; + /** + * Reposition the Flash object to cover the currently activated element. + * + * @returns `undefined` + * @private + */ + var _reposition = function() { + var htmlBridge; + if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { + var pos = _getDOMObjectPosition(_currentElement); + _extend(htmlBridge.style, { + width: pos.width + "px", + height: pos.height + "px", + top: pos.top + "px", + left: pos.left + "px", + zIndex: "" + _getSafeZIndex(_globalConfig.zIndex) + }); + } + }; + /** + * Sends a signal to the Flash object to display the hand cursor if `true`. + * + * @returns `undefined` + * @private + */ + var _setHandCursor = function(enabled) { + if (_flashState.ready === true) { + if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === "function") { + _flashState.bridge.setHandCursor(enabled); + } else { + _flashState.ready = false; + } + } + }; + /** + * Get a safe value for `zIndex` + * + * @returns an integer, or "auto" + * @private + */ + var _getSafeZIndex = function(val) { + if (/^(?:auto|inherit)$/.test(val)) { + return val; + } + var zIndex; + if (typeof val === "number" && !_isNaN(val)) { + zIndex = val; + } else if (typeof val === "string") { + zIndex = _getSafeZIndex(_parseInt(val, 10)); + } + return typeof zIndex === "number" ? zIndex : "auto"; + }; + /** + * Detect the Flash Player status, version, and plugin type. + * + * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} + * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} + * + * @returns `undefined` + * @private + */ + var _detectFlashSupport = function(ActiveXObject) { + var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = ""; + /** + * Derived from Apple's suggested sniffer. + * @param {String} desc e.g. "Shockwave Flash 7.0 r61" + * @returns {String} "7.0.61" + * @private + */ + function parseFlashVersion(desc) { + var matches = desc.match(/[\d]+/g); + matches.length = 3; + return matches.join("."); + } + function isPepperFlash(flashPlayerFileName) { + return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === "chrome.plugin"); + } + function inspectPlugin(plugin) { + if (plugin) { + hasFlash = true; + if (plugin.version) { + flashVersion = parseFlashVersion(plugin.version); + } + if (!flashVersion && plugin.description) { + flashVersion = parseFlashVersion(plugin.description); + } + if (plugin.filename) { + isPPAPI = isPepperFlash(plugin.filename); + } + } + } + if (_navigator.plugins && _navigator.plugins.length) { + plugin = _navigator.plugins["Shockwave Flash"]; + inspectPlugin(plugin); + if (_navigator.plugins["Shockwave Flash 2.0"]) { + hasFlash = true; + flashVersion = "2.0.0.11"; + } + } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) { + mimeType = _navigator.mimeTypes["application/x-shockwave-flash"]; + plugin = mimeType && mimeType.enabledPlugin; + inspectPlugin(plugin); + } else if (typeof ActiveXObject !== "undefined") { + isActiveX = true; + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e1) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); + hasFlash = true; + flashVersion = "6.0.21"; + } catch (e2) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e3) { + isActiveX = false; + } + } + } + } + _flashState.disabled = hasFlash !== true; + _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion); + _flashState.version = flashVersion || "0.0.0"; + _flashState.pluginType = isPPAPI ? "pepper" : isActiveX ? "activex" : hasFlash ? "netscape" : "unknown"; + }; + /** + * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. + */ + _detectFlashSupport(_ActiveXObject); + /** + * A shell constructor for `ZeroClipboard` client instances. + * + * @constructor + */ + var ZeroClipboard = function() { + if (!(this instanceof ZeroClipboard)) { + return new ZeroClipboard(); + } + if (typeof ZeroClipboard._createClient === "function") { + ZeroClipboard._createClient.apply(this, _args(arguments)); + } + }; + /** + * The ZeroClipboard library's version number. + * + * @static + * @readonly + * @property {string} + */ + _defineProperty(ZeroClipboard, "version", { + value: "2.1.6", + writable: false, + configurable: true, + enumerable: true + }); + /** + * Update or get a copy of the ZeroClipboard global configuration. + * Returns a copy of the current/updated configuration. + * + * @returns Object + * @static + */ + ZeroClipboard.config = function() { + return _config.apply(this, _args(arguments)); + }; + /** + * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard. + * + * @returns Object + * @static + */ + ZeroClipboard.state = function() { + return _state.apply(this, _args(arguments)); + }; + /** + * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc. + * + * @returns Boolean + * @static + */ + ZeroClipboard.isFlashUnusable = function() { + return _isFlashUnusable.apply(this, _args(arguments)); + }; + /** + * Register an event listener. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.on = function() { + return _on.apply(this, _args(arguments)); + }; + /** + * Unregister an event listener. + * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`. + * If no `eventType` is provided, it will unregister all listeners for every event type. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.off = function() { + return _off.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType`. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.handlers = function() { + return _listeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + * @static + */ + ZeroClipboard.emit = function() { + return _emit.apply(this, _args(arguments)); + }; + /** + * Create and embed the Flash object. + * + * @returns The Flash object + * @static + */ + ZeroClipboard.create = function() { + return _create.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything, including the embedded Flash object. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.destroy = function() { + return _destroy.apply(this, _args(arguments)); + }; + /** + * Set the pending data for clipboard injection. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.setData = function() { + return _setData.apply(this, _args(arguments)); + }; + /** + * Clear the pending data for clipboard injection. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.clearData = function() { + return _clearData.apply(this, _args(arguments)); + }; + /** + * Get a copy of the pending data for clipboard injection. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + * @static + */ + ZeroClipboard.getData = function() { + return _getData.apply(this, _args(arguments)); + }; + /** + * Sets the current HTML object that the Flash object should overlay. This will put the global + * Flash object on top of the current element; depending on the setup, this may also set the + * pending clipboard text data as well as the Flash object's wrapping element's title attribute + * based on the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.focus = ZeroClipboard.activate = function() { + return _focus.apply(this, _args(arguments)); + }; + /** + * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on + * the setup, this may also unset the Flash object's wrapping element's title attribute based on + * the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.blur = ZeroClipboard.deactivate = function() { + return _blur.apply(this, _args(arguments)); + }; + /** + * Returns the currently focused/"activated" HTML element that the Flash object is wrapping. + * + * @returns `HTMLElement` or `null` + * @static + */ + ZeroClipboard.activeElement = function() { + return _activeElement.apply(this, _args(arguments)); + }; + /** + * Keep track of the ZeroClipboard client instance counter. + */ + var _clientIdCounter = 0; + /** + * Keep track of the state of the client instances. + * + * Entry structure: + * _clientMeta[client.id] = { + * instance: client, + * elements: [], + * handlers: {} + * }; + */ + var _clientMeta = {}; + /** + * Keep track of the ZeroClipboard clipped elements counter. + */ + var _elementIdCounter = 0; + /** + * Keep track of the state of the clipped element relationships to clients. + * + * Entry structure: + * _elementMeta[element.zcClippingId] = [client1.id, client2.id]; + */ + var _elementMeta = {}; + /** + * Keep track of the state of the mouse event handlers for clipped elements. + * + * Entry structure: + * _mouseHandlers[element.zcClippingId] = { + * mouseover: function(event) {}, + * mouseout: function(event) {}, + * mouseenter: function(event) {}, + * mouseleave: function(event) {}, + * mousemove: function(event) {} + * }; + */ + var _mouseHandlers = {}; + /** + * Extending the ZeroClipboard configuration defaults for the Client module. + */ + _extend(_globalConfig, { + autoActivate: true + }); + /** + * The real constructor for `ZeroClipboard` client instances. + * @private + */ + var _clientConstructor = function(elements) { + var client = this; + client.id = "" + _clientIdCounter++; + _clientMeta[client.id] = { + instance: client, + elements: [], + handlers: {} + }; + if (elements) { + client.clip(elements); + } + ZeroClipboard.on("*", function(event) { + return client.emit(event); + }); + ZeroClipboard.on("destroy", function() { + client.destroy(); + }); + ZeroClipboard.create(); + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.on`. + * @private + */ + var _clientOn = function(eventType, listener) { + var i, len, events, added = {}, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + this.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!handlers[eventType]) { + handlers[eventType] = []; + } + handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + this.emit({ + type: "ready", + client: this + }); + } + if (added.error) { + var errorTypes = [ "disabled", "outdated", "unavailable", "deactivated", "overdue" ]; + for (i = 0, len = errorTypes.length; i < len; i++) { + if (_flashState[errorTypes[i]]) { + this.emit({ + type: "error", + name: "flash-" + errorTypes[i], + client: this + }); + break; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.off`. + * @private + */ + var _clientOff = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; + if (arguments.length === 0) { + events = _keys(handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + this.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`. + * @private + */ + var _clientListeners = function(eventType) { + var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; + if (handlers) { + if (typeof eventType === "string" && eventType) { + copy = handlers[eventType] ? handlers[eventType].slice(0) : []; + } else { + copy = _deepCopy(handlers); + } + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.emit`. + * @private + */ + var _clientEmit = function(event) { + if (_clientShouldEmit.call(this, event)) { + if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + event = _extend({}, event); + } + var eventCopy = _extend({}, _createEvent(event), { + client: this + }); + _clientDispatchCallbacks.call(this, eventCopy); + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.clip`. + * @private + */ + var _clientClip = function(elements) { + elements = _prepClip(elements); + for (var i = 0; i < elements.length; i++) { + if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { + if (!elements[i].zcClippingId) { + elements[i].zcClippingId = "zcClippingId_" + _elementIdCounter++; + _elementMeta[elements[i].zcClippingId] = [ this.id ]; + if (_globalConfig.autoActivate === true) { + _addMouseHandlers(elements[i]); + } + } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) { + _elementMeta[elements[i].zcClippingId].push(this.id); + } + var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements; + if (clippedElements.indexOf(elements[i]) === -1) { + clippedElements.push(elements[i]); + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`. + * @private + */ + var _clientUnclip = function(elements) { + var meta = _clientMeta[this.id]; + if (!meta) { + return this; + } + var clippedElements = meta.elements; + var arrayIndex; + if (typeof elements === "undefined") { + elements = clippedElements.slice(0); + } else { + elements = _prepClip(elements); + } + for (var i = elements.length; i--; ) { + if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { + arrayIndex = 0; + while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) { + clippedElements.splice(arrayIndex, 1); + } + var clientIds = _elementMeta[elements[i].zcClippingId]; + if (clientIds) { + arrayIndex = 0; + while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) { + clientIds.splice(arrayIndex, 1); + } + if (clientIds.length === 0) { + if (_globalConfig.autoActivate === true) { + _removeMouseHandlers(elements[i]); + } + delete elements[i].zcClippingId; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.elements`. + * @private + */ + var _clientElements = function() { + var meta = _clientMeta[this.id]; + return meta && meta.elements ? meta.elements.slice(0) : []; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`. + * @private + */ + var _clientDestroy = function() { + this.unclip(); + this.off(); + delete _clientMeta[this.id]; + }; + /** + * Inspect an Event to see if the Client (`this`) should honor it for emission. + * @private + */ + var _clientShouldEmit = function(event) { + if (!(event && event.type)) { + return false; + } + if (event.client && event.client !== this) { + return false; + } + var clippedEls = _clientMeta[this.id] && _clientMeta[this.id].elements; + var hasClippedEls = !!clippedEls && clippedEls.length > 0; + var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1; + var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1; + var goodClient = event.client && event.client === this; + if (!(goodTarget || goodRelTarget || goodClient)) { + return false; + } + return true; + }; + /** + * Handle the actual dispatching of events to a client instance. + * + * @returns `this` + * @private + */ + var _clientDispatchCallbacks = function(event) { + if (!(typeof event === "object" && event && event.type)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = _clientMeta[this.id] && _clientMeta[this.id].handlers["*"] || []; + var specificTypeHandlers = _clientMeta[this.id] && _clientMeta[this.id].handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + return this; + }; + /** + * Prepares the elements for clipping/unclipping. + * + * @returns An Array of elements. + * @private + */ + var _prepClip = function(elements) { + if (typeof elements === "string") { + elements = []; + } + return typeof elements.length !== "number" ? [ elements ] : elements; + }; + /** + * Add a `mouseover` handler function for a clipped element. + * + * @returns `undefined` + * @private + */ + var _addMouseHandlers = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + var _suppressMouseEvents = function(event) { + if (!(event || (event = _window.event))) { + return; + } + if (event._source !== "js") { + event.stopImmediatePropagation(); + event.preventDefault(); + } + delete event._source; + }; + var _elementMouseOver = function(event) { + if (!(event || (event = _window.event))) { + return; + } + _suppressMouseEvents(event); + ZeroClipboard.focus(element); + }; + element.addEventListener("mouseover", _elementMouseOver, false); + element.addEventListener("mouseout", _suppressMouseEvents, false); + element.addEventListener("mouseenter", _suppressMouseEvents, false); + element.addEventListener("mouseleave", _suppressMouseEvents, false); + element.addEventListener("mousemove", _suppressMouseEvents, false); + _mouseHandlers[element.zcClippingId] = { + mouseover: _elementMouseOver, + mouseout: _suppressMouseEvents, + mouseenter: _suppressMouseEvents, + mouseleave: _suppressMouseEvents, + mousemove: _suppressMouseEvents + }; + }; + /** + * Remove a `mouseover` handler function for a clipped element. + * + * @returns `undefined` + * @private + */ + var _removeMouseHandlers = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + var mouseHandlers = _mouseHandlers[element.zcClippingId]; + if (!(typeof mouseHandlers === "object" && mouseHandlers)) { + return; + } + var key, val, mouseEvents = [ "move", "leave", "enter", "out", "over" ]; + for (var i = 0, len = mouseEvents.length; i < len; i++) { + key = "mouse" + mouseEvents[i]; + val = mouseHandlers[key]; + if (typeof val === "function") { + element.removeEventListener(key, val, false); + } + } + delete _mouseHandlers[element.zcClippingId]; + }; + /** + * Creates a new ZeroClipboard client instance. + * Optionally, auto-`clip` an element or collection of elements. + * + * @constructor + */ + ZeroClipboard._createClient = function() { + _clientConstructor.apply(this, _args(arguments)); + }; + /** + * Register an event listener to the client. + * + * @returns `this` + */ + ZeroClipboard.prototype.on = function() { + return _clientOn.apply(this, _args(arguments)); + }; + /** + * Unregister an event handler from the client. + * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`. + * If no `eventType` is provided, it will unregister all handlers for every event type. + * + * @returns `this` + */ + ZeroClipboard.prototype.off = function() { + return _clientOff.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType` from the client. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.prototype.handlers = function() { + return _clientListeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object for this client's registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + */ + ZeroClipboard.prototype.emit = function() { + return _clientEmit.apply(this, _args(arguments)); + }; + /** + * Register clipboard actions for new element(s) to the client. + * + * @returns `this` + */ + ZeroClipboard.prototype.clip = function() { + return _clientClip.apply(this, _args(arguments)); + }; + /** + * Unregister the clipboard actions of previously registered element(s) on the page. + * If no elements are provided, ALL registered elements will be unregistered. + * + * @returns `this` + */ + ZeroClipboard.prototype.unclip = function() { + return _clientUnclip.apply(this, _args(arguments)); + }; + /** + * Get all of the elements to which this client is clipped. + * + * @returns array of clipped elements + */ + ZeroClipboard.prototype.elements = function() { + return _clientElements.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything for a single client. + * This will NOT destroy the embedded Flash object. + * + * @returns `undefined` + */ + ZeroClipboard.prototype.destroy = function() { + return _clientDestroy.apply(this, _args(arguments)); + }; + /** + * Stores the pending plain text to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setText = function(text) { + ZeroClipboard.setData("text/plain", text); + return this; + }; + /** + * Stores the pending HTML text to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setHtml = function(html) { + ZeroClipboard.setData("text/html", html); + return this; + }; + /** + * Stores the pending rich text (RTF) to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setRichText = function(richText) { + ZeroClipboard.setData("application/rtf", richText); + return this; + }; + /** + * Stores the pending data to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setData = function() { + ZeroClipboard.setData.apply(this, _args(arguments)); + return this; + }; + /** + * Clears the pending data to inject into the clipboard. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `this` + */ + ZeroClipboard.prototype.clearData = function() { + ZeroClipboard.clearData.apply(this, _args(arguments)); + return this; + }; + /** + * Gets a copy of the pending data to inject into the clipboard. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + */ + ZeroClipboard.prototype.getData = function() { + return ZeroClipboard.getData.apply(this, _args(arguments)); + }; + if (typeof define === "function" && define.amd) { + define(function() { + return ZeroClipboard; + }); + } else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) { + module.exports = ZeroClipboard; + } else { + window.ZeroClipboard = ZeroClipboard; + } +})(function() { + return this || window; +}());
\ No newline at end of file diff --git a/addons/website_links/static/src/js/ZeroClipboard.min.map b/addons/website_links/static/src/js/ZeroClipboard.min.map new file mode 100755 index 00000000000..854758fb439 --- /dev/null +++ b/addons/website_links/static/src/js/ZeroClipboard.min.map @@ -0,0 +1 @@ +{"version":3,"file":"ZeroClipboard.min.js","sources":["ZeroClipboard.js"],"names":["window","undefined","_currentElement","_copyTarget","_window","_document","document","_navigator","navigator","_setTimeout","setTimeout","_encodeURIComponent","encodeURIComponent","_ActiveXObject","ActiveXObject","_Error","Error","_parseInt","Number","parseInt","_parseFloat","parseFloat","_isNaN","isNaN","_round","Math","round","_now","Date","now","_keys","Object","keys","_defineProperty","defineProperty","_hasOwn","prototype","hasOwnProperty","_slice","Array","slice","_unwrap","unwrapper","el","wrap","unwrap","div","createElement","unwrappedDiv","nodeType","e","_args","argumentsObj","call","_extend","i","len","arg","prop","src","copy","args","arguments","target","length","_deepCopy","source","_pick","obj","newObj","_omit","indexOf","_deleteOwnProperties","_containedBy","ancestorEl","ownerDocument","parentNode","_getDirPathOfUrl","url","dir","split","lastIndexOf","_getCurrentScriptUrlFromErrorStack","stack","matches","match","_getCurrentScriptUrlFromError","err","sourceURL","fileName","_getCurrentScriptUrl","jsPath","scripts","currentScript","getElementsByTagName","readyState","_getUnanimousScriptParentDir","jsDir","_getDefaultSwfPath","_flashState","bridge","version","pluginType","disabled","outdated","unavailable","deactivated","overdue","ready","_minimumFlashVersion","_handlers","_clipData","_clipDataFormatMap","_eventMessages","error","flash-disabled","flash-outdated","flash-unavailable","flash-deactivated","flash-overdue","_globalConfig","swfPath","trustedDomains","location","host","cacheBust","forceEnhancedClipboard","flashLoadTimeout","autoActivate","bubbleEvents","containerId","containerClass","swfObjectId","hoverClass","activeClass","forceHandCursor","title","zIndex","_config","options","test","_isValidHtml4Id","_state","browser","flash","zeroclipboard","ZeroClipboard","config","_isFlashUnusable","_on","eventType","listener","events","added","toLowerCase","on","replace","push","emit","type","errorTypes","name","_off","foundIndex","perEventHandlers","off","splice","_listeners","_emit","event","eventCopy","returnVal","tmp","_createEvent","_preprocessEvent","_dispatchCallbacks","this","_mapClipDataToFlash","data","formatMap","_create","isFlashUnusable","maxWait","_embedSwf","_destroy","clearData","blur","_unembedSwf","_setData","format","dataObj","dataFormat","_clearData","_getData","_focus","element","_removeClass","_addClass","newTitle","getAttribute","htmlBridge","_getHtmlBridge","setAttribute","useHandCursor","_getStyle","_setHandCursor","_reposition","_blur","removeAttribute","style","left","top","width","_activeElement","id","relatedTarget","currentTarget","timeStamp","msg","message","minimumVersion","clipboardData","setData","_mapClipResultsFromFlash","_getRelatedTarget","_addMouseData","targetEl","relatedTargetId","getElementById","srcElement","fromElement","toElement","pos","_getDOMObjectPosition","screenLeft","screenX","screenTop","screenY","scrollLeft","body","documentElement","scrollTop","pageX","_stageX","pageY","_stageY","clientX","clientY","moveX","movementX","moveY","movementY","x","y","offsetX","offsetY","layerX","layerY","_shouldPerformAsync","_dispatchCallback","func","context","async","apply","wildcardTypeHandlers","specificTypeHandlers","handlers","concat","originalContext","handleEvent","sourceIsSwf","_source","flashErrorNames","wasDeactivated","textContent","htmlContent","value","outerHTML","innerHTML","innerText","_safeActiveElement","focus","_fireMouseEvent","bubbles","cancelable","doc","defaults","view","defaultView","canBubble","detail","button","which","createEvent","dispatchEvent","ctrlKey","altKey","shiftKey","metaKey","initMouseEvent","_createHtmlBridge","container","className","position","height","_getSafeZIndex","flashBridge","nodeName","allowScriptAccess","_determineScriptAccess","allowNetworking","flashvars","_vars","swfUrl","_cacheBust","divToBeReplaced","appendChild","tmpDiv","oldIE","firstChild","replaceChild","display","removeSwfFromIE","removeChild","clipData","newClipData","text","html","rtf","clipResults","newResults","tmpHash","path","domain","domains","str","trustedOriginsExpanded","_extractDomain","protocol","join","originOrUrl","protocolIndex","pathIndex","_extractAllDomains","origins","resultsArray","currentDomain","configOptions","swfDomain","activeElement","classList","contains","add","classNames","setClass","c","cl","remove","getComputedStyle","getPropertyValue","_getZoomFactor","rect","physicalWidth","logicalWidth","zoomFactor","getBoundingClientRect","right","offsetWidth","info","pageXOffset","pageYOffset","leftBorderWidth","clientLeft","topBorderWidth","clientTop","bottom","enabled","setHandCursor","val","_detectFlashSupport","parseFlashVersion","desc","isPepperFlash","flashPlayerFileName","inspectPlugin","plugin","hasFlash","flashVersion","description","filename","isPPAPI","ax","mimeType","isActiveX","plugins","mimeTypes","enabledPlugin","GetVariable","e1","e2","e3","_createClient","writable","configurable","enumerable","state","create","destroy","getData","activate","deactivate","_clientIdCounter","_clientMeta","_elementIdCounter","_elementMeta","_mouseHandlers","_clientConstructor","elements","client","instance","clip","_clientOn","_clientOff","_clientListeners","_clientEmit","_clientShouldEmit","_clientDispatchCallbacks","_clientClip","_prepClip","zcClippingId","_addMouseHandlers","clippedElements","_clientUnclip","meta","arrayIndex","clientIds","_removeMouseHandlers","_clientElements","_clientDestroy","unclip","clippedEls","hasClippedEls","goodTarget","goodRelTarget","goodClient","_suppressMouseEvents","stopImmediatePropagation","preventDefault","_elementMouseOver","addEventListener","mouseover","mouseout","mouseenter","mouseleave","mousemove","mouseHandlers","key","mouseEvents","removeEventListener","setText","setHtml","setRichText","richText","define","amd","module","exports"],"mappings":"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","sourcesContent":["/*!\n * ZeroClipboard\n * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.\n * Copyright (c) 2014 Jon Rohan, James M. Greene\n * Licensed MIT\n * http://zeroclipboard.org/\n * v2.1.6\n */\n(function(window, undefined) {\n \"use strict\";\n /**\n * Store references to critically important global functions that may be\n * overridden on certain web pages.\n */\n var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _round = _window.Math.round, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() {\n var unwrapper = function(el) {\n return el;\n };\n if (typeof _window.wrap === \"function\" && typeof _window.unwrap === \"function\") {\n try {\n var div = _document.createElement(\"div\");\n var unwrappedDiv = _window.unwrap(div);\n if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) {\n unwrapper = _window.unwrap;\n }\n } catch (e) {}\n }\n return unwrapper;\n }();\n /**\n * Convert an `arguments` object into an Array.\n *\n * @returns The arguments as an Array\n * @private\n */\n var _args = function(argumentsObj) {\n return _slice.call(argumentsObj, 0);\n };\n /**\n * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`.\n *\n * @returns The target object, augmented\n * @private\n */\n var _extend = function() {\n var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {};\n for (i = 1, len = args.length; i < len; i++) {\n if ((arg = args[i]) != null) {\n for (prop in arg) {\n if (_hasOwn.call(arg, prop)) {\n src = target[prop];\n copy = arg[prop];\n if (target !== copy && copy !== undefined) {\n target[prop] = copy;\n }\n }\n }\n }\n }\n return target;\n };\n /**\n * Return a deep copy of the source object or array.\n *\n * @returns Object or Array\n * @private\n */\n var _deepCopy = function(source) {\n var copy, i, len, prop;\n if (typeof source !== \"object\" || source == null) {\n copy = source;\n } else if (typeof source.length === \"number\") {\n copy = [];\n for (i = 0, len = source.length; i < len; i++) {\n if (_hasOwn.call(source, i)) {\n copy[i] = _deepCopy(source[i]);\n }\n }\n } else {\n copy = {};\n for (prop in source) {\n if (_hasOwn.call(source, prop)) {\n copy[prop] = _deepCopy(source[prop]);\n }\n }\n }\n return copy;\n };\n /**\n * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep.\n * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to\n * be kept.\n *\n * @returns A new filtered object.\n * @private\n */\n var _pick = function(obj, keys) {\n var newObj = {};\n for (var i = 0, len = keys.length; i < len; i++) {\n if (keys[i] in obj) {\n newObj[keys[i]] = obj[keys[i]];\n }\n }\n return newObj;\n };\n /**\n * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit.\n * The inverse of `_pick`.\n *\n * @returns A new filtered object.\n * @private\n */\n var _omit = function(obj, keys) {\n var newObj = {};\n for (var prop in obj) {\n if (keys.indexOf(prop) === -1) {\n newObj[prop] = obj[prop];\n }\n }\n return newObj;\n };\n /**\n * Remove all owned, enumerable properties from an object.\n *\n * @returns The original object without its owned, enumerable properties.\n * @private\n */\n var _deleteOwnProperties = function(obj) {\n if (obj) {\n for (var prop in obj) {\n if (_hasOwn.call(obj, prop)) {\n delete obj[prop];\n }\n }\n }\n return obj;\n };\n /**\n * Determine if an element is contained within another element.\n *\n * @returns Boolean\n * @private\n */\n var _containedBy = function(el, ancestorEl) {\n if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) {\n do {\n if (el === ancestorEl) {\n return true;\n }\n el = el.parentNode;\n } while (el);\n }\n return false;\n };\n /**\n * Get the URL path's parent directory.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getDirPathOfUrl = function(url) {\n var dir;\n if (typeof url === \"string\" && url) {\n dir = url.split(\"#\")[0].split(\"?\")[0];\n dir = url.slice(0, url.lastIndexOf(\"/\") + 1);\n }\n return dir;\n };\n /**\n * Get the current script's URL by throwing an `Error` and analyzing it.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getCurrentScriptUrlFromErrorStack = function(stack) {\n var url, matches;\n if (typeof stack === \"string\" && stack) {\n matches = stack.match(/^(?:|[^:@]*@|.+\\)@(?=http[s]?|file)|.+?\\s+(?: at |@)(?:[^:\\(]+ )*[\\(]?)((?:http[s]?|file):\\/\\/[\\/]?.+?\\/[^:\\)]*?)(?::\\d+)(?::\\d+)?/);\n if (matches && matches[1]) {\n url = matches[1];\n } else {\n matches = stack.match(/\\)@((?:http[s]?|file):\\/\\/[\\/]?.+?\\/[^:\\)]*?)(?::\\d+)(?::\\d+)?/);\n if (matches && matches[1]) {\n url = matches[1];\n }\n }\n }\n return url;\n };\n /**\n * Get the current script's URL by throwing an `Error` and analyzing it.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getCurrentScriptUrlFromError = function() {\n var url, err;\n try {\n throw new _Error();\n } catch (e) {\n err = e;\n }\n if (err) {\n url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack);\n }\n return url;\n };\n /**\n * Get the current script's URL.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getCurrentScriptUrl = function() {\n var jsPath, scripts, i;\n if (_document.currentScript && (jsPath = _document.currentScript.src)) {\n return jsPath;\n }\n scripts = _document.getElementsByTagName(\"script\");\n if (scripts.length === 1) {\n return scripts[0].src || undefined;\n }\n if (\"readyState\" in scripts[0]) {\n for (i = scripts.length; i--; ) {\n if (scripts[i].readyState === \"interactive\" && (jsPath = scripts[i].src)) {\n return jsPath;\n }\n }\n }\n if (_document.readyState === \"loading\" && (jsPath = scripts[scripts.length - 1].src)) {\n return jsPath;\n }\n if (jsPath = _getCurrentScriptUrlFromError()) {\n return jsPath;\n }\n return undefined;\n };\n /**\n * Get the unanimous parent directory of ALL script tags.\n * If any script tags are either (a) inline or (b) from differing parent\n * directories, this method must return `undefined`.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getUnanimousScriptParentDir = function() {\n var i, jsDir, jsPath, scripts = _document.getElementsByTagName(\"script\");\n for (i = scripts.length; i--; ) {\n if (!(jsPath = scripts[i].src)) {\n jsDir = null;\n break;\n }\n jsPath = _getDirPathOfUrl(jsPath);\n if (jsDir == null) {\n jsDir = jsPath;\n } else if (jsDir !== jsPath) {\n jsDir = null;\n break;\n }\n }\n return jsDir || undefined;\n };\n /**\n * Get the presumed location of the \"ZeroClipboard.swf\" file, based on the location\n * of the executing JavaScript file (e.g. \"ZeroClipboard.js\", etc.).\n *\n * @returns String\n * @private\n */\n var _getDefaultSwfPath = function() {\n var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || \"\";\n return jsDir + \"ZeroClipboard.swf\";\n };\n /**\n * Keep track of the state of the Flash object.\n * @private\n */\n var _flashState = {\n bridge: null,\n version: \"0.0.0\",\n pluginType: \"unknown\",\n disabled: null,\n outdated: null,\n unavailable: null,\n deactivated: null,\n overdue: null,\n ready: null\n };\n /**\n * The minimum Flash Player version required to use ZeroClipboard completely.\n * @readonly\n * @private\n */\n var _minimumFlashVersion = \"11.0.0\";\n /**\n * Keep track of all event listener registrations.\n * @private\n */\n var _handlers = {};\n /**\n * Keep track of the currently activated element.\n * @private\n */\n var _currentElement;\n /**\n * Keep track of the element that was activated when a `copy` process started.\n * @private\n */\n var _copyTarget;\n /**\n * Keep track of data for the pending clipboard transaction.\n * @private\n */\n var _clipData = {};\n /**\n * Keep track of data formats for the pending clipboard transaction.\n * @private\n */\n var _clipDataFormatMap = null;\n /**\n * The `message` store for events\n * @private\n */\n var _eventMessages = {\n ready: \"Flash communication is established\",\n error: {\n \"flash-disabled\": \"Flash is disabled or not installed\",\n \"flash-outdated\": \"Flash is too outdated to support ZeroClipboard\",\n \"flash-unavailable\": \"Flash is unable to communicate bidirectionally with JavaScript\",\n \"flash-deactivated\": \"Flash is too outdated for your browser and/or is configured as click-to-activate\",\n \"flash-overdue\": \"Flash communication was established but NOT within the acceptable time limit\"\n }\n };\n /**\n * ZeroClipboard configuration defaults for the Core module.\n * @private\n */\n var _globalConfig = {\n swfPath: _getDefaultSwfPath(),\n trustedDomains: window.location.host ? [ window.location.host ] : [],\n cacheBust: true,\n forceEnhancedClipboard: false,\n flashLoadTimeout: 3e4,\n autoActivate: true,\n bubbleEvents: true,\n containerId: \"global-zeroclipboard-html-bridge\",\n containerClass: \"global-zeroclipboard-container\",\n swfObjectId: \"global-zeroclipboard-flash-bridge\",\n hoverClass: \"zeroclipboard-is-hover\",\n activeClass: \"zeroclipboard-is-active\",\n forceHandCursor: false,\n title: null,\n zIndex: 999999999\n };\n /**\n * The underlying implementation of `ZeroClipboard.config`.\n * @private\n */\n var _config = function(options) {\n if (typeof options === \"object\" && options !== null) {\n for (var prop in options) {\n if (_hasOwn.call(options, prop)) {\n if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) {\n _globalConfig[prop] = options[prop];\n } else if (_flashState.bridge == null) {\n if (prop === \"containerId\" || prop === \"swfObjectId\") {\n if (_isValidHtml4Id(options[prop])) {\n _globalConfig[prop] = options[prop];\n } else {\n throw new Error(\"The specified `\" + prop + \"` value is not valid as an HTML4 Element ID\");\n }\n } else {\n _globalConfig[prop] = options[prop];\n }\n }\n }\n }\n }\n if (typeof options === \"string\" && options) {\n if (_hasOwn.call(_globalConfig, options)) {\n return _globalConfig[options];\n }\n return;\n }\n return _deepCopy(_globalConfig);\n };\n /**\n * The underlying implementation of `ZeroClipboard.state`.\n * @private\n */\n var _state = function() {\n return {\n browser: _pick(_navigator, [ \"userAgent\", \"platform\", \"appName\" ]),\n flash: _omit(_flashState, [ \"bridge\" ]),\n zeroclipboard: {\n version: ZeroClipboard.version,\n config: ZeroClipboard.config()\n }\n };\n };\n /**\n * The underlying implementation of `ZeroClipboard.isFlashUnusable`.\n * @private\n */\n var _isFlashUnusable = function() {\n return !!(_flashState.disabled || _flashState.outdated || _flashState.unavailable || _flashState.deactivated);\n };\n /**\n * The underlying implementation of `ZeroClipboard.on`.\n * @private\n */\n var _on = function(eventType, listener) {\n var i, len, events, added = {};\n if (typeof eventType === \"string\" && eventType) {\n events = eventType.toLowerCase().split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n ZeroClipboard.on(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].replace(/^on/, \"\");\n added[eventType] = true;\n if (!_handlers[eventType]) {\n _handlers[eventType] = [];\n }\n _handlers[eventType].push(listener);\n }\n if (added.ready && _flashState.ready) {\n ZeroClipboard.emit({\n type: \"ready\"\n });\n }\n if (added.error) {\n var errorTypes = [ \"disabled\", \"outdated\", \"unavailable\", \"deactivated\", \"overdue\" ];\n for (i = 0, len = errorTypes.length; i < len; i++) {\n if (_flashState[errorTypes[i]] === true) {\n ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-\" + errorTypes[i]\n });\n break;\n }\n }\n }\n }\n return ZeroClipboard;\n };\n /**\n * The underlying implementation of `ZeroClipboard.off`.\n * @private\n */\n var _off = function(eventType, listener) {\n var i, len, foundIndex, events, perEventHandlers;\n if (arguments.length === 0) {\n events = _keys(_handlers);\n } else if (typeof eventType === \"string\" && eventType) {\n events = eventType.split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n ZeroClipboard.off(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].toLowerCase().replace(/^on/, \"\");\n perEventHandlers = _handlers[eventType];\n if (perEventHandlers && perEventHandlers.length) {\n if (listener) {\n foundIndex = perEventHandlers.indexOf(listener);\n while (foundIndex !== -1) {\n perEventHandlers.splice(foundIndex, 1);\n foundIndex = perEventHandlers.indexOf(listener, foundIndex);\n }\n } else {\n perEventHandlers.length = 0;\n }\n }\n }\n }\n return ZeroClipboard;\n };\n /**\n * The underlying implementation of `ZeroClipboard.handlers`.\n * @private\n */\n var _listeners = function(eventType) {\n var copy;\n if (typeof eventType === \"string\" && eventType) {\n copy = _deepCopy(_handlers[eventType]) || null;\n } else {\n copy = _deepCopy(_handlers);\n }\n return copy;\n };\n /**\n * The underlying implementation of `ZeroClipboard.emit`.\n * @private\n */\n var _emit = function(event) {\n var eventCopy, returnVal, tmp;\n event = _createEvent(event);\n if (!event) {\n return;\n }\n if (_preprocessEvent(event)) {\n return;\n }\n if (event.type === \"ready\" && _flashState.overdue === true) {\n return ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-overdue\"\n });\n }\n eventCopy = _extend({}, event);\n _dispatchCallbacks.call(this, eventCopy);\n if (event.type === \"copy\") {\n tmp = _mapClipDataToFlash(_clipData);\n returnVal = tmp.data;\n _clipDataFormatMap = tmp.formatMap;\n }\n return returnVal;\n };\n /**\n * The underlying implementation of `ZeroClipboard.create`.\n * @private\n */\n var _create = function() {\n if (typeof _flashState.ready !== \"boolean\") {\n _flashState.ready = false;\n }\n if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) {\n var maxWait = _globalConfig.flashLoadTimeout;\n if (typeof maxWait === \"number\" && maxWait >= 0) {\n _setTimeout(function() {\n if (typeof _flashState.deactivated !== \"boolean\") {\n _flashState.deactivated = true;\n }\n if (_flashState.deactivated === true) {\n ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-deactivated\"\n });\n }\n }, maxWait);\n }\n _flashState.overdue = false;\n _embedSwf();\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.destroy`.\n * @private\n */\n var _destroy = function() {\n ZeroClipboard.clearData();\n ZeroClipboard.blur();\n ZeroClipboard.emit(\"destroy\");\n _unembedSwf();\n ZeroClipboard.off();\n };\n /**\n * The underlying implementation of `ZeroClipboard.setData`.\n * @private\n */\n var _setData = function(format, data) {\n var dataObj;\n if (typeof format === \"object\" && format && typeof data === \"undefined\") {\n dataObj = format;\n ZeroClipboard.clearData();\n } else if (typeof format === \"string\" && format) {\n dataObj = {};\n dataObj[format] = data;\n } else {\n return;\n }\n for (var dataFormat in dataObj) {\n if (typeof dataFormat === \"string\" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === \"string\" && dataObj[dataFormat]) {\n _clipData[dataFormat] = dataObj[dataFormat];\n }\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.clearData`.\n * @private\n */\n var _clearData = function(format) {\n if (typeof format === \"undefined\") {\n _deleteOwnProperties(_clipData);\n _clipDataFormatMap = null;\n } else if (typeof format === \"string\" && _hasOwn.call(_clipData, format)) {\n delete _clipData[format];\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.getData`.\n * @private\n */\n var _getData = function(format) {\n if (typeof format === \"undefined\") {\n return _deepCopy(_clipData);\n } else if (typeof format === \"string\" && _hasOwn.call(_clipData, format)) {\n return _clipData[format];\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`.\n * @private\n */\n var _focus = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n if (_currentElement) {\n _removeClass(_currentElement, _globalConfig.activeClass);\n if (_currentElement !== element) {\n _removeClass(_currentElement, _globalConfig.hoverClass);\n }\n }\n _currentElement = element;\n _addClass(element, _globalConfig.hoverClass);\n var newTitle = element.getAttribute(\"title\") || _globalConfig.title;\n if (typeof newTitle === \"string\" && newTitle) {\n var htmlBridge = _getHtmlBridge(_flashState.bridge);\n if (htmlBridge) {\n htmlBridge.setAttribute(\"title\", newTitle);\n }\n }\n var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, \"cursor\") === \"pointer\";\n _setHandCursor(useHandCursor);\n _reposition();\n };\n /**\n * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`.\n * @private\n */\n var _blur = function() {\n var htmlBridge = _getHtmlBridge(_flashState.bridge);\n if (htmlBridge) {\n htmlBridge.removeAttribute(\"title\");\n htmlBridge.style.left = \"0px\";\n htmlBridge.style.top = \"-9999px\";\n htmlBridge.style.width = \"1px\";\n htmlBridge.style.top = \"1px\";\n }\n if (_currentElement) {\n _removeClass(_currentElement, _globalConfig.hoverClass);\n _removeClass(_currentElement, _globalConfig.activeClass);\n _currentElement = null;\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.activeElement`.\n * @private\n */\n var _activeElement = function() {\n return _currentElement || null;\n };\n /**\n * Check if a value is a valid HTML4 `ID` or `Name` token.\n * @private\n */\n var _isValidHtml4Id = function(id) {\n return typeof id === \"string\" && id && /^[A-Za-z][A-Za-z0-9_:\\-\\.]*$/.test(id);\n };\n /**\n * Create or update an `event` object, based on the `eventType`.\n * @private\n */\n var _createEvent = function(event) {\n var eventType;\n if (typeof event === \"string\" && event) {\n eventType = event;\n event = {};\n } else if (typeof event === \"object\" && event && typeof event.type === \"string\" && event.type) {\n eventType = event.type;\n }\n if (!eventType) {\n return;\n }\n if (!event.target && /^(copy|aftercopy|_click)$/.test(eventType.toLowerCase())) {\n event.target = _copyTarget;\n }\n _extend(event, {\n type: eventType.toLowerCase(),\n target: event.target || _currentElement || null,\n relatedTarget: event.relatedTarget || null,\n currentTarget: _flashState && _flashState.bridge || null,\n timeStamp: event.timeStamp || _now() || null\n });\n var msg = _eventMessages[event.type];\n if (event.type === \"error\" && event.name && msg) {\n msg = msg[event.name];\n }\n if (msg) {\n event.message = msg;\n }\n if (event.type === \"ready\") {\n _extend(event, {\n target: null,\n version: _flashState.version\n });\n }\n if (event.type === \"error\") {\n if (/^flash-(disabled|outdated|unavailable|deactivated|overdue)$/.test(event.name)) {\n _extend(event, {\n target: null,\n minimumVersion: _minimumFlashVersion\n });\n }\n if (/^flash-(outdated|unavailable|deactivated|overdue)$/.test(event.name)) {\n _extend(event, {\n version: _flashState.version\n });\n }\n }\n if (event.type === \"copy\") {\n event.clipboardData = {\n setData: ZeroClipboard.setData,\n clearData: ZeroClipboard.clearData\n };\n }\n if (event.type === \"aftercopy\") {\n event = _mapClipResultsFromFlash(event, _clipDataFormatMap);\n }\n if (event.target && !event.relatedTarget) {\n event.relatedTarget = _getRelatedTarget(event.target);\n }\n event = _addMouseData(event);\n return event;\n };\n /**\n * Get a relatedTarget from the target's `data-clipboard-target` attribute\n * @private\n */\n var _getRelatedTarget = function(targetEl) {\n var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute(\"data-clipboard-target\");\n return relatedTargetId ? _document.getElementById(relatedTargetId) : null;\n };\n /**\n * Add element and position data to `MouseEvent` instances\n * @private\n */\n var _addMouseData = function(event) {\n if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) {\n var srcElement = event.target;\n var fromElement = event.type === \"_mouseover\" && event.relatedTarget ? event.relatedTarget : undefined;\n var toElement = event.type === \"_mouseout\" && event.relatedTarget ? event.relatedTarget : undefined;\n var pos = _getDOMObjectPosition(srcElement);\n var screenLeft = _window.screenLeft || _window.screenX || 0;\n var screenTop = _window.screenTop || _window.screenY || 0;\n var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft;\n var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop;\n var pageX = pos.left + (typeof event._stageX === \"number\" ? event._stageX : 0);\n var pageY = pos.top + (typeof event._stageY === \"number\" ? event._stageY : 0);\n var clientX = pageX - scrollLeft;\n var clientY = pageY - scrollTop;\n var screenX = screenLeft + clientX;\n var screenY = screenTop + clientY;\n var moveX = typeof event.movementX === \"number\" ? event.movementX : 0;\n var moveY = typeof event.movementY === \"number\" ? event.movementY : 0;\n delete event._stageX;\n delete event._stageY;\n _extend(event, {\n srcElement: srcElement,\n fromElement: fromElement,\n toElement: toElement,\n screenX: screenX,\n screenY: screenY,\n pageX: pageX,\n pageY: pageY,\n clientX: clientX,\n clientY: clientY,\n x: clientX,\n y: clientY,\n movementX: moveX,\n movementY: moveY,\n offsetX: 0,\n offsetY: 0,\n layerX: 0,\n layerY: 0\n });\n }\n return event;\n };\n /**\n * Determine if an event's registered handlers should be execute synchronously or asynchronously.\n *\n * @returns {boolean}\n * @private\n */\n var _shouldPerformAsync = function(event) {\n var eventType = event && typeof event.type === \"string\" && event.type || \"\";\n return !/^(?:(?:before)?copy|destroy)$/.test(eventType);\n };\n /**\n * Control if a callback should be executed asynchronously or not.\n *\n * @returns `undefined`\n * @private\n */\n var _dispatchCallback = function(func, context, args, async) {\n if (async) {\n _setTimeout(function() {\n func.apply(context, args);\n }, 0);\n } else {\n func.apply(context, args);\n }\n };\n /**\n * Handle the actual dispatching of events to client instances.\n *\n * @returns `undefined`\n * @private\n */\n var _dispatchCallbacks = function(event) {\n if (!(typeof event === \"object\" && event && event.type)) {\n return;\n }\n var async = _shouldPerformAsync(event);\n var wildcardTypeHandlers = _handlers[\"*\"] || [];\n var specificTypeHandlers = _handlers[event.type] || [];\n var handlers = wildcardTypeHandlers.concat(specificTypeHandlers);\n if (handlers && handlers.length) {\n var i, len, func, context, eventCopy, originalContext = this;\n for (i = 0, len = handlers.length; i < len; i++) {\n func = handlers[i];\n context = originalContext;\n if (typeof func === \"string\" && typeof _window[func] === \"function\") {\n func = _window[func];\n }\n if (typeof func === \"object\" && func && typeof func.handleEvent === \"function\") {\n context = func;\n func = func.handleEvent;\n }\n if (typeof func === \"function\") {\n eventCopy = _extend({}, event);\n _dispatchCallback(func, context, [ eventCopy ], async);\n }\n }\n }\n return this;\n };\n /**\n * Preprocess any special behaviors, reactions, or state changes after receiving this event.\n * Executes only once per event emitted, NOT once per client.\n * @private\n */\n var _preprocessEvent = function(event) {\n var element = event.target || _currentElement || null;\n var sourceIsSwf = event._source === \"swf\";\n delete event._source;\n var flashErrorNames = [ \"flash-disabled\", \"flash-outdated\", \"flash-unavailable\", \"flash-deactivated\", \"flash-overdue\" ];\n switch (event.type) {\n case \"error\":\n if (flashErrorNames.indexOf(event.name) !== -1) {\n _extend(_flashState, {\n disabled: event.name === \"flash-disabled\",\n outdated: event.name === \"flash-outdated\",\n unavailable: event.name === \"flash-unavailable\",\n deactivated: event.name === \"flash-deactivated\",\n overdue: event.name === \"flash-overdue\",\n ready: false\n });\n }\n break;\n\n case \"ready\":\n var wasDeactivated = _flashState.deactivated === true;\n _extend(_flashState, {\n disabled: false,\n outdated: false,\n unavailable: false,\n deactivated: false,\n overdue: wasDeactivated,\n ready: !wasDeactivated\n });\n break;\n\n case \"beforecopy\":\n _copyTarget = element;\n break;\n\n case \"copy\":\n var textContent, htmlContent, targetEl = event.relatedTarget;\n if (!(_clipData[\"text/html\"] || _clipData[\"text/plain\"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) {\n event.clipboardData.clearData();\n event.clipboardData.setData(\"text/plain\", textContent);\n if (htmlContent !== textContent) {\n event.clipboardData.setData(\"text/html\", htmlContent);\n }\n } else if (!_clipData[\"text/plain\"] && event.target && (textContent = event.target.getAttribute(\"data-clipboard-text\"))) {\n event.clipboardData.clearData();\n event.clipboardData.setData(\"text/plain\", textContent);\n }\n break;\n\n case \"aftercopy\":\n ZeroClipboard.clearData();\n if (element && element !== _safeActiveElement() && element.focus) {\n element.focus();\n }\n break;\n\n case \"_mouseover\":\n ZeroClipboard.focus(element);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) {\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseenter\",\n bubbles: false,\n cancelable: false\n }));\n }\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseover\"\n }));\n }\n break;\n\n case \"_mouseout\":\n ZeroClipboard.blur();\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) {\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseleave\",\n bubbles: false,\n cancelable: false\n }));\n }\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseout\"\n }));\n }\n break;\n\n case \"_mousedown\":\n _addClass(element, _globalConfig.activeClass);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_mouseup\":\n _removeClass(element, _globalConfig.activeClass);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_click\":\n _copyTarget = null;\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_mousemove\":\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n }\n if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) {\n return true;\n }\n };\n /**\n * Dispatch a synthetic MouseEvent.\n *\n * @returns `undefined`\n * @private\n */\n var _fireMouseEvent = function(event) {\n if (!(event && typeof event.type === \"string\" && event)) {\n return;\n }\n var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = {\n view: doc.defaultView || _window,\n canBubble: true,\n cancelable: true,\n detail: event.type === \"click\" ? 1 : 0,\n button: typeof event.which === \"number\" ? event.which - 1 : typeof event.button === \"number\" ? event.button : doc.createEvent ? 0 : 1\n }, args = _extend(defaults, event);\n if (!target) {\n return;\n }\n if (doc.createEvent && target.dispatchEvent) {\n args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ];\n e = doc.createEvent(\"MouseEvents\");\n if (e.initMouseEvent) {\n e.initMouseEvent.apply(e, args);\n e._source = \"js\";\n target.dispatchEvent(e);\n }\n }\n };\n /**\n * Create the HTML bridge element to embed the Flash object into.\n * @private\n */\n var _createHtmlBridge = function() {\n var container = _document.createElement(\"div\");\n container.id = _globalConfig.containerId;\n container.className = _globalConfig.containerClass;\n container.style.position = \"absolute\";\n container.style.left = \"0px\";\n container.style.top = \"-9999px\";\n container.style.width = \"1px\";\n container.style.height = \"1px\";\n container.style.zIndex = \"\" + _getSafeZIndex(_globalConfig.zIndex);\n return container;\n };\n /**\n * Get the HTML element container that wraps the Flash bridge object/element.\n * @private\n */\n var _getHtmlBridge = function(flashBridge) {\n var htmlBridge = flashBridge && flashBridge.parentNode;\n while (htmlBridge && htmlBridge.nodeName === \"OBJECT\" && htmlBridge.parentNode) {\n htmlBridge = htmlBridge.parentNode;\n }\n return htmlBridge || null;\n };\n /**\n * Create the SWF object.\n *\n * @returns The SWF object reference.\n * @private\n */\n var _embedSwf = function() {\n var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge);\n if (!flashBridge) {\n var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig);\n var allowNetworking = allowScriptAccess === \"never\" ? \"none\" : \"all\";\n var flashvars = _vars(_globalConfig);\n var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig);\n container = _createHtmlBridge();\n var divToBeReplaced = _document.createElement(\"div\");\n container.appendChild(divToBeReplaced);\n _document.body.appendChild(container);\n var tmpDiv = _document.createElement(\"div\");\n var oldIE = _flashState.pluginType === \"activex\";\n tmpDiv.innerHTML = '<object id=\"' + _globalConfig.swfObjectId + '\" name=\"' + _globalConfig.swfObjectId + '\" ' + 'width=\"100%\" height=\"100%\" ' + (oldIE ? 'classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\"' : 'type=\"application/x-shockwave-flash\" data=\"' + swfUrl + '\"') + \">\" + (oldIE ? '<param name=\"movie\" value=\"' + swfUrl + '\"/>' : \"\") + '<param name=\"allowScriptAccess\" value=\"' + allowScriptAccess + '\"/>' + '<param name=\"allowNetworking\" value=\"' + allowNetworking + '\"/>' + '<param name=\"menu\" value=\"false\"/>' + '<param name=\"wmode\" value=\"transparent\"/>' + '<param name=\"flashvars\" value=\"' + flashvars + '\"/>' + \"</object>\";\n flashBridge = tmpDiv.firstChild;\n tmpDiv = null;\n _unwrap(flashBridge).ZeroClipboard = ZeroClipboard;\n container.replaceChild(flashBridge, divToBeReplaced);\n }\n if (!flashBridge) {\n flashBridge = _document[_globalConfig.swfObjectId];\n if (flashBridge && (len = flashBridge.length)) {\n flashBridge = flashBridge[len - 1];\n }\n if (!flashBridge && container) {\n flashBridge = container.firstChild;\n }\n }\n _flashState.bridge = flashBridge || null;\n return flashBridge;\n };\n /**\n * Destroy the SWF object.\n * @private\n */\n var _unembedSwf = function() {\n var flashBridge = _flashState.bridge;\n if (flashBridge) {\n var htmlBridge = _getHtmlBridge(flashBridge);\n if (htmlBridge) {\n if (_flashState.pluginType === \"activex\" && \"readyState\" in flashBridge) {\n flashBridge.style.display = \"none\";\n (function removeSwfFromIE() {\n if (flashBridge.readyState === 4) {\n for (var prop in flashBridge) {\n if (typeof flashBridge[prop] === \"function\") {\n flashBridge[prop] = null;\n }\n }\n if (flashBridge.parentNode) {\n flashBridge.parentNode.removeChild(flashBridge);\n }\n if (htmlBridge.parentNode) {\n htmlBridge.parentNode.removeChild(htmlBridge);\n }\n } else {\n _setTimeout(removeSwfFromIE, 10);\n }\n })();\n } else {\n if (flashBridge.parentNode) {\n flashBridge.parentNode.removeChild(flashBridge);\n }\n if (htmlBridge.parentNode) {\n htmlBridge.parentNode.removeChild(htmlBridge);\n }\n }\n }\n _flashState.ready = null;\n _flashState.bridge = null;\n _flashState.deactivated = null;\n }\n };\n /**\n * Map the data format names of the \"clipData\" to Flash-friendly names.\n *\n * @returns A new transformed object.\n * @private\n */\n var _mapClipDataToFlash = function(clipData) {\n var newClipData = {}, formatMap = {};\n if (!(typeof clipData === \"object\" && clipData)) {\n return;\n }\n for (var dataFormat in clipData) {\n if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === \"string\" && clipData[dataFormat]) {\n switch (dataFormat.toLowerCase()) {\n case \"text/plain\":\n case \"text\":\n case \"air:text\":\n case \"flash:text\":\n newClipData.text = clipData[dataFormat];\n formatMap.text = dataFormat;\n break;\n\n case \"text/html\":\n case \"html\":\n case \"air:html\":\n case \"flash:html\":\n newClipData.html = clipData[dataFormat];\n formatMap.html = dataFormat;\n break;\n\n case \"application/rtf\":\n case \"text/rtf\":\n case \"rtf\":\n case \"richtext\":\n case \"air:rtf\":\n case \"flash:rtf\":\n newClipData.rtf = clipData[dataFormat];\n formatMap.rtf = dataFormat;\n break;\n\n default:\n break;\n }\n }\n }\n return {\n data: newClipData,\n formatMap: formatMap\n };\n };\n /**\n * Map the data format names from Flash-friendly names back to their original \"clipData\" names (via a format mapping).\n *\n * @returns A new transformed object.\n * @private\n */\n var _mapClipResultsFromFlash = function(clipResults, formatMap) {\n if (!(typeof clipResults === \"object\" && clipResults && typeof formatMap === \"object\" && formatMap)) {\n return clipResults;\n }\n var newResults = {};\n for (var prop in clipResults) {\n if (_hasOwn.call(clipResults, prop)) {\n if (prop !== \"success\" && prop !== \"data\") {\n newResults[prop] = clipResults[prop];\n continue;\n }\n newResults[prop] = {};\n var tmpHash = clipResults[prop];\n for (var dataFormat in tmpHash) {\n if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) {\n newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat];\n }\n }\n }\n }\n return newResults;\n };\n /**\n * Will look at a path, and will create a \"?noCache={time}\" or \"&noCache={time}\"\n * query param string to return. Does NOT append that string to the original path.\n * This is useful because ExternalInterface often breaks when a Flash SWF is cached.\n *\n * @returns The `noCache` query param with necessary \"?\"/\"&\" prefix.\n * @private\n */\n var _cacheBust = function(path, options) {\n var cacheBust = options == null || options && options.cacheBust === true;\n if (cacheBust) {\n return (path.indexOf(\"?\") === -1 ? \"?\" : \"&\") + \"noCache=\" + _now();\n } else {\n return \"\";\n }\n };\n /**\n * Creates a query string for the FlashVars param.\n * Does NOT include the cache-busting query param.\n *\n * @returns FlashVars query string\n * @private\n */\n var _vars = function(options) {\n var i, len, domain, domains, str = \"\", trustedOriginsExpanded = [];\n if (options.trustedDomains) {\n if (typeof options.trustedDomains === \"string\") {\n domains = [ options.trustedDomains ];\n } else if (typeof options.trustedDomains === \"object\" && \"length\" in options.trustedDomains) {\n domains = options.trustedDomains;\n }\n }\n if (domains && domains.length) {\n for (i = 0, len = domains.length; i < len; i++) {\n if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === \"string\") {\n domain = _extractDomain(domains[i]);\n if (!domain) {\n continue;\n }\n if (domain === \"*\") {\n trustedOriginsExpanded.length = 0;\n trustedOriginsExpanded.push(domain);\n break;\n }\n trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, \"//\" + domain, _window.location.protocol + \"//\" + domain ]);\n }\n }\n }\n if (trustedOriginsExpanded.length) {\n str += \"trustedOrigins=\" + _encodeURIComponent(trustedOriginsExpanded.join(\",\"));\n }\n if (options.forceEnhancedClipboard === true) {\n str += (str ? \"&\" : \"\") + \"forceEnhancedClipboard=true\";\n }\n if (typeof options.swfObjectId === \"string\" && options.swfObjectId) {\n str += (str ? \"&\" : \"\") + \"swfObjectId=\" + _encodeURIComponent(options.swfObjectId);\n }\n return str;\n };\n /**\n * Extract the domain (e.g. \"github.com\") from an origin (e.g. \"https://github.com\") or\n * URL (e.g. \"https://github.com/zeroclipboard/zeroclipboard/\").\n *\n * @returns the domain\n * @private\n */\n var _extractDomain = function(originOrUrl) {\n if (originOrUrl == null || originOrUrl === \"\") {\n return null;\n }\n originOrUrl = originOrUrl.replace(/^\\s+|\\s+$/g, \"\");\n if (originOrUrl === \"\") {\n return null;\n }\n var protocolIndex = originOrUrl.indexOf(\"//\");\n originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2);\n var pathIndex = originOrUrl.indexOf(\"/\");\n originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex);\n if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === \".swf\") {\n return null;\n }\n return originOrUrl || null;\n };\n /**\n * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`.\n *\n * @returns The appropriate script access level.\n * @private\n */\n var _determineScriptAccess = function() {\n var _extractAllDomains = function(origins) {\n var i, len, tmp, resultsArray = [];\n if (typeof origins === \"string\") {\n origins = [ origins ];\n }\n if (!(typeof origins === \"object\" && origins && typeof origins.length === \"number\")) {\n return resultsArray;\n }\n for (i = 0, len = origins.length; i < len; i++) {\n if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) {\n if (tmp === \"*\") {\n resultsArray.length = 0;\n resultsArray.push(\"*\");\n break;\n }\n if (resultsArray.indexOf(tmp) === -1) {\n resultsArray.push(tmp);\n }\n }\n }\n return resultsArray;\n };\n return function(currentDomain, configOptions) {\n var swfDomain = _extractDomain(configOptions.swfPath);\n if (swfDomain === null) {\n swfDomain = currentDomain;\n }\n var trustedDomains = _extractAllDomains(configOptions.trustedDomains);\n var len = trustedDomains.length;\n if (len > 0) {\n if (len === 1 && trustedDomains[0] === \"*\") {\n return \"always\";\n }\n if (trustedDomains.indexOf(currentDomain) !== -1) {\n if (len === 1 && currentDomain === swfDomain) {\n return \"sameDomain\";\n }\n return \"always\";\n }\n }\n return \"never\";\n };\n }();\n /**\n * Get the currently active/focused DOM element.\n *\n * @returns the currently active/focused element, or `null`\n * @private\n */\n var _safeActiveElement = function() {\n try {\n return _document.activeElement;\n } catch (err) {\n return null;\n }\n };\n /**\n * Add a class to an element, if it doesn't already have it.\n *\n * @returns The element, with its new class added.\n * @private\n */\n var _addClass = function(element, value) {\n if (!element || element.nodeType !== 1) {\n return element;\n }\n if (element.classList) {\n if (!element.classList.contains(value)) {\n element.classList.add(value);\n }\n return element;\n }\n if (value && typeof value === \"string\") {\n var classNames = (value || \"\").split(/\\s+/);\n if (element.nodeType === 1) {\n if (!element.className) {\n element.className = value;\n } else {\n var className = \" \" + element.className + \" \", setClass = element.className;\n for (var c = 0, cl = classNames.length; c < cl; c++) {\n if (className.indexOf(\" \" + classNames[c] + \" \") < 0) {\n setClass += \" \" + classNames[c];\n }\n }\n element.className = setClass.replace(/^\\s+|\\s+$/g, \"\");\n }\n }\n }\n return element;\n };\n /**\n * Remove a class from an element, if it has it.\n *\n * @returns The element, with its class removed.\n * @private\n */\n var _removeClass = function(element, value) {\n if (!element || element.nodeType !== 1) {\n return element;\n }\n if (element.classList) {\n if (element.classList.contains(value)) {\n element.classList.remove(value);\n }\n return element;\n }\n if (typeof value === \"string\" && value) {\n var classNames = value.split(/\\s+/);\n if (element.nodeType === 1 && element.className) {\n var className = (\" \" + element.className + \" \").replace(/[\\n\\t]/g, \" \");\n for (var c = 0, cl = classNames.length; c < cl; c++) {\n className = className.replace(\" \" + classNames[c] + \" \", \" \");\n }\n element.className = className.replace(/^\\s+|\\s+$/g, \"\");\n }\n }\n return element;\n };\n /**\n * Attempt to interpret the element's CSS styling. If `prop` is `\"cursor\"`,\n * then we assume that it should be a hand (\"pointer\") cursor if the element\n * is an anchor element (\"a\" tag).\n *\n * @returns The computed style property.\n * @private\n */\n var _getStyle = function(el, prop) {\n var value = _window.getComputedStyle(el, null).getPropertyValue(prop);\n if (prop === \"cursor\") {\n if (!value || value === \"auto\") {\n if (el.nodeName === \"A\") {\n return \"pointer\";\n }\n }\n }\n return value;\n };\n /**\n * Get the zoom factor of the browser. Always returns `1.0`, except at\n * non-default zoom levels in IE<8 and some older versions of WebKit.\n *\n * @returns Floating unit percentage of the zoom factor (e.g. 150% = `1.5`).\n * @private\n */\n var _getZoomFactor = function() {\n var rect, physicalWidth, logicalWidth, zoomFactor = 1;\n if (typeof _document.body.getBoundingClientRect === \"function\") {\n rect = _document.body.getBoundingClientRect();\n physicalWidth = rect.right - rect.left;\n logicalWidth = _document.body.offsetWidth;\n zoomFactor = _round(physicalWidth / logicalWidth * 100) / 100;\n }\n return zoomFactor;\n };\n /**\n * Get the DOM positioning info of an element.\n *\n * @returns Object containing the element's position, width, and height.\n * @private\n */\n var _getDOMObjectPosition = function(obj) {\n var info = {\n left: 0,\n top: 0,\n width: 0,\n height: 0\n };\n if (obj.getBoundingClientRect) {\n var rect = obj.getBoundingClientRect();\n var pageXOffset, pageYOffset, zoomFactor;\n if (\"pageXOffset\" in _window && \"pageYOffset\" in _window) {\n pageXOffset = _window.pageXOffset;\n pageYOffset = _window.pageYOffset;\n } else {\n zoomFactor = _getZoomFactor();\n pageXOffset = _round(_document.documentElement.scrollLeft / zoomFactor);\n pageYOffset = _round(_document.documentElement.scrollTop / zoomFactor);\n }\n var leftBorderWidth = _document.documentElement.clientLeft || 0;\n var topBorderWidth = _document.documentElement.clientTop || 0;\n info.left = rect.left + pageXOffset - leftBorderWidth;\n info.top = rect.top + pageYOffset - topBorderWidth;\n info.width = \"width\" in rect ? rect.width : rect.right - rect.left;\n info.height = \"height\" in rect ? rect.height : rect.bottom - rect.top;\n }\n return info;\n };\n /**\n * Reposition the Flash object to cover the currently activated element.\n *\n * @returns `undefined`\n * @private\n */\n var _reposition = function() {\n var htmlBridge;\n if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) {\n var pos = _getDOMObjectPosition(_currentElement);\n _extend(htmlBridge.style, {\n width: pos.width + \"px\",\n height: pos.height + \"px\",\n top: pos.top + \"px\",\n left: pos.left + \"px\",\n zIndex: \"\" + _getSafeZIndex(_globalConfig.zIndex)\n });\n }\n };\n /**\n * Sends a signal to the Flash object to display the hand cursor if `true`.\n *\n * @returns `undefined`\n * @private\n */\n var _setHandCursor = function(enabled) {\n if (_flashState.ready === true) {\n if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === \"function\") {\n _flashState.bridge.setHandCursor(enabled);\n } else {\n _flashState.ready = false;\n }\n }\n };\n /**\n * Get a safe value for `zIndex`\n *\n * @returns an integer, or \"auto\"\n * @private\n */\n var _getSafeZIndex = function(val) {\n if (/^(?:auto|inherit)$/.test(val)) {\n return val;\n }\n var zIndex;\n if (typeof val === \"number\" && !_isNaN(val)) {\n zIndex = val;\n } else if (typeof val === \"string\") {\n zIndex = _getSafeZIndex(_parseInt(val, 10));\n }\n return typeof zIndex === \"number\" ? zIndex : \"auto\";\n };\n /**\n * Detect the Flash Player status, version, and plugin type.\n *\n * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code}\n * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript}\n *\n * @returns `undefined`\n * @private\n */\n var _detectFlashSupport = function(ActiveXObject) {\n var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = \"\";\n /**\n * Derived from Apple's suggested sniffer.\n * @param {String} desc e.g. \"Shockwave Flash 7.0 r61\"\n * @returns {String} \"7.0.61\"\n * @private\n */\n function parseFlashVersion(desc) {\n var matches = desc.match(/[\\d]+/g);\n matches.length = 3;\n return matches.join(\".\");\n }\n function isPepperFlash(flashPlayerFileName) {\n return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\\.dll|libpepflashplayer\\.so|pepperflashplayer\\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === \"chrome.plugin\");\n }\n function inspectPlugin(plugin) {\n if (plugin) {\n hasFlash = true;\n if (plugin.version) {\n flashVersion = parseFlashVersion(plugin.version);\n }\n if (!flashVersion && plugin.description) {\n flashVersion = parseFlashVersion(plugin.description);\n }\n if (plugin.filename) {\n isPPAPI = isPepperFlash(plugin.filename);\n }\n }\n }\n if (_navigator.plugins && _navigator.plugins.length) {\n plugin = _navigator.plugins[\"Shockwave Flash\"];\n inspectPlugin(plugin);\n if (_navigator.plugins[\"Shockwave Flash 2.0\"]) {\n hasFlash = true;\n flashVersion = \"2.0.0.11\";\n }\n } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) {\n mimeType = _navigator.mimeTypes[\"application/x-shockwave-flash\"];\n plugin = mimeType && mimeType.enabledPlugin;\n inspectPlugin(plugin);\n } else if (typeof ActiveXObject !== \"undefined\") {\n isActiveX = true;\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash.7\");\n hasFlash = true;\n flashVersion = parseFlashVersion(ax.GetVariable(\"$version\"));\n } catch (e1) {\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash.6\");\n hasFlash = true;\n flashVersion = \"6.0.21\";\n } catch (e2) {\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash\");\n hasFlash = true;\n flashVersion = parseFlashVersion(ax.GetVariable(\"$version\"));\n } catch (e3) {\n isActiveX = false;\n }\n }\n }\n }\n _flashState.disabled = hasFlash !== true;\n _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion);\n _flashState.version = flashVersion || \"0.0.0\";\n _flashState.pluginType = isPPAPI ? \"pepper\" : isActiveX ? \"activex\" : hasFlash ? \"netscape\" : \"unknown\";\n };\n /**\n * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later.\n */\n _detectFlashSupport(_ActiveXObject);\n /**\n * A shell constructor for `ZeroClipboard` client instances.\n *\n * @constructor\n */\n var ZeroClipboard = function() {\n if (!(this instanceof ZeroClipboard)) {\n return new ZeroClipboard();\n }\n if (typeof ZeroClipboard._createClient === \"function\") {\n ZeroClipboard._createClient.apply(this, _args(arguments));\n }\n };\n /**\n * The ZeroClipboard library's version number.\n *\n * @static\n * @readonly\n * @property {string}\n */\n _defineProperty(ZeroClipboard, \"version\", {\n value: \"2.1.6\",\n writable: false,\n configurable: true,\n enumerable: true\n });\n /**\n * Update or get a copy of the ZeroClipboard global configuration.\n * Returns a copy of the current/updated configuration.\n *\n * @returns Object\n * @static\n */\n ZeroClipboard.config = function() {\n return _config.apply(this, _args(arguments));\n };\n /**\n * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard.\n *\n * @returns Object\n * @static\n */\n ZeroClipboard.state = function() {\n return _state.apply(this, _args(arguments));\n };\n /**\n * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc.\n *\n * @returns Boolean\n * @static\n */\n ZeroClipboard.isFlashUnusable = function() {\n return _isFlashUnusable.apply(this, _args(arguments));\n };\n /**\n * Register an event listener.\n *\n * @returns `ZeroClipboard`\n * @static\n */\n ZeroClipboard.on = function() {\n return _on.apply(this, _args(arguments));\n };\n /**\n * Unregister an event listener.\n * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`.\n * If no `eventType` is provided, it will unregister all listeners for every event type.\n *\n * @returns `ZeroClipboard`\n * @static\n */\n ZeroClipboard.off = function() {\n return _off.apply(this, _args(arguments));\n };\n /**\n * Retrieve event listeners for an `eventType`.\n * If no `eventType` is provided, it will retrieve all listeners for every event type.\n *\n * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`\n */\n ZeroClipboard.handlers = function() {\n return _listeners.apply(this, _args(arguments));\n };\n /**\n * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners.\n *\n * @returns For the \"copy\" event, returns the Flash-friendly \"clipData\" object; otherwise `undefined`.\n * @static\n */\n ZeroClipboard.emit = function() {\n return _emit.apply(this, _args(arguments));\n };\n /**\n * Create and embed the Flash object.\n *\n * @returns The Flash object\n * @static\n */\n ZeroClipboard.create = function() {\n return _create.apply(this, _args(arguments));\n };\n /**\n * Self-destruct and clean up everything, including the embedded Flash object.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.destroy = function() {\n return _destroy.apply(this, _args(arguments));\n };\n /**\n * Set the pending data for clipboard injection.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.setData = function() {\n return _setData.apply(this, _args(arguments));\n };\n /**\n * Clear the pending data for clipboard injection.\n * If no `format` is provided, all pending data formats will be cleared.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.clearData = function() {\n return _clearData.apply(this, _args(arguments));\n };\n /**\n * Get a copy of the pending data for clipboard injection.\n * If no `format` is provided, a copy of ALL pending data formats will be returned.\n *\n * @returns `String` or `Object`\n * @static\n */\n ZeroClipboard.getData = function() {\n return _getData.apply(this, _args(arguments));\n };\n /**\n * Sets the current HTML object that the Flash object should overlay. This will put the global\n * Flash object on top of the current element; depending on the setup, this may also set the\n * pending clipboard text data as well as the Flash object's wrapping element's title attribute\n * based on the underlying HTML element and ZeroClipboard configuration.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.focus = ZeroClipboard.activate = function() {\n return _focus.apply(this, _args(arguments));\n };\n /**\n * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on\n * the setup, this may also unset the Flash object's wrapping element's title attribute based on\n * the underlying HTML element and ZeroClipboard configuration.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.blur = ZeroClipboard.deactivate = function() {\n return _blur.apply(this, _args(arguments));\n };\n /**\n * Returns the currently focused/\"activated\" HTML element that the Flash object is wrapping.\n *\n * @returns `HTMLElement` or `null`\n * @static\n */\n ZeroClipboard.activeElement = function() {\n return _activeElement.apply(this, _args(arguments));\n };\n /**\n * Keep track of the ZeroClipboard client instance counter.\n */\n var _clientIdCounter = 0;\n /**\n * Keep track of the state of the client instances.\n *\n * Entry structure:\n * _clientMeta[client.id] = {\n * instance: client,\n * elements: [],\n * handlers: {}\n * };\n */\n var _clientMeta = {};\n /**\n * Keep track of the ZeroClipboard clipped elements counter.\n */\n var _elementIdCounter = 0;\n /**\n * Keep track of the state of the clipped element relationships to clients.\n *\n * Entry structure:\n * _elementMeta[element.zcClippingId] = [client1.id, client2.id];\n */\n var _elementMeta = {};\n /**\n * Keep track of the state of the mouse event handlers for clipped elements.\n *\n * Entry structure:\n * _mouseHandlers[element.zcClippingId] = {\n * mouseover: function(event) {},\n * mouseout: function(event) {},\n * mouseenter: function(event) {},\n * mouseleave: function(event) {},\n * mousemove: function(event) {}\n * };\n */\n var _mouseHandlers = {};\n /**\n * Extending the ZeroClipboard configuration defaults for the Client module.\n */\n _extend(_globalConfig, {\n autoActivate: true\n });\n /**\n * The real constructor for `ZeroClipboard` client instances.\n * @private\n */\n var _clientConstructor = function(elements) {\n var client = this;\n client.id = \"\" + _clientIdCounter++;\n _clientMeta[client.id] = {\n instance: client,\n elements: [],\n handlers: {}\n };\n if (elements) {\n client.clip(elements);\n }\n ZeroClipboard.on(\"*\", function(event) {\n return client.emit(event);\n });\n ZeroClipboard.on(\"destroy\", function() {\n client.destroy();\n });\n ZeroClipboard.create();\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.on`.\n * @private\n */\n var _clientOn = function(eventType, listener) {\n var i, len, events, added = {}, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;\n if (typeof eventType === \"string\" && eventType) {\n events = eventType.toLowerCase().split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n this.on(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].replace(/^on/, \"\");\n added[eventType] = true;\n if (!handlers[eventType]) {\n handlers[eventType] = [];\n }\n handlers[eventType].push(listener);\n }\n if (added.ready && _flashState.ready) {\n this.emit({\n type: \"ready\",\n client: this\n });\n }\n if (added.error) {\n var errorTypes = [ \"disabled\", \"outdated\", \"unavailable\", \"deactivated\", \"overdue\" ];\n for (i = 0, len = errorTypes.length; i < len; i++) {\n if (_flashState[errorTypes[i]]) {\n this.emit({\n type: \"error\",\n name: \"flash-\" + errorTypes[i],\n client: this\n });\n break;\n }\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.off`.\n * @private\n */\n var _clientOff = function(eventType, listener) {\n var i, len, foundIndex, events, perEventHandlers, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;\n if (arguments.length === 0) {\n events = _keys(handlers);\n } else if (typeof eventType === \"string\" && eventType) {\n events = eventType.split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n this.off(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].toLowerCase().replace(/^on/, \"\");\n perEventHandlers = handlers[eventType];\n if (perEventHandlers && perEventHandlers.length) {\n if (listener) {\n foundIndex = perEventHandlers.indexOf(listener);\n while (foundIndex !== -1) {\n perEventHandlers.splice(foundIndex, 1);\n foundIndex = perEventHandlers.indexOf(listener, foundIndex);\n }\n } else {\n perEventHandlers.length = 0;\n }\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`.\n * @private\n */\n var _clientListeners = function(eventType) {\n var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;\n if (handlers) {\n if (typeof eventType === \"string\" && eventType) {\n copy = handlers[eventType] ? handlers[eventType].slice(0) : [];\n } else {\n copy = _deepCopy(handlers);\n }\n }\n return copy;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.emit`.\n * @private\n */\n var _clientEmit = function(event) {\n if (_clientShouldEmit.call(this, event)) {\n if (typeof event === \"object\" && event && typeof event.type === \"string\" && event.type) {\n event = _extend({}, event);\n }\n var eventCopy = _extend({}, _createEvent(event), {\n client: this\n });\n _clientDispatchCallbacks.call(this, eventCopy);\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.clip`.\n * @private\n */\n var _clientClip = function(elements) {\n elements = _prepClip(elements);\n for (var i = 0; i < elements.length; i++) {\n if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) {\n if (!elements[i].zcClippingId) {\n elements[i].zcClippingId = \"zcClippingId_\" + _elementIdCounter++;\n _elementMeta[elements[i].zcClippingId] = [ this.id ];\n if (_globalConfig.autoActivate === true) {\n _addMouseHandlers(elements[i]);\n }\n } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) {\n _elementMeta[elements[i].zcClippingId].push(this.id);\n }\n var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements;\n if (clippedElements.indexOf(elements[i]) === -1) {\n clippedElements.push(elements[i]);\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`.\n * @private\n */\n var _clientUnclip = function(elements) {\n var meta = _clientMeta[this.id];\n if (!meta) {\n return this;\n }\n var clippedElements = meta.elements;\n var arrayIndex;\n if (typeof elements === \"undefined\") {\n elements = clippedElements.slice(0);\n } else {\n elements = _prepClip(elements);\n }\n for (var i = elements.length; i--; ) {\n if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) {\n arrayIndex = 0;\n while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) {\n clippedElements.splice(arrayIndex, 1);\n }\n var clientIds = _elementMeta[elements[i].zcClippingId];\n if (clientIds) {\n arrayIndex = 0;\n while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) {\n clientIds.splice(arrayIndex, 1);\n }\n if (clientIds.length === 0) {\n if (_globalConfig.autoActivate === true) {\n _removeMouseHandlers(elements[i]);\n }\n delete elements[i].zcClippingId;\n }\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.elements`.\n * @private\n */\n var _clientElements = function() {\n var meta = _clientMeta[this.id];\n return meta && meta.elements ? meta.elements.slice(0) : [];\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`.\n * @private\n */\n var _clientDestroy = function() {\n this.unclip();\n this.off();\n delete _clientMeta[this.id];\n };\n /**\n * Inspect an Event to see if the Client (`this`) should honor it for emission.\n * @private\n */\n var _clientShouldEmit = function(event) {\n if (!(event && event.type)) {\n return false;\n }\n if (event.client && event.client !== this) {\n return false;\n }\n var clippedEls = _clientMeta[this.id] && _clientMeta[this.id].elements;\n var hasClippedEls = !!clippedEls && clippedEls.length > 0;\n var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1;\n var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1;\n var goodClient = event.client && event.client === this;\n if (!(goodTarget || goodRelTarget || goodClient)) {\n return false;\n }\n return true;\n };\n /**\n * Handle the actual dispatching of events to a client instance.\n *\n * @returns `this`\n * @private\n */\n var _clientDispatchCallbacks = function(event) {\n if (!(typeof event === \"object\" && event && event.type)) {\n return;\n }\n var async = _shouldPerformAsync(event);\n var wildcardTypeHandlers = _clientMeta[this.id] && _clientMeta[this.id].handlers[\"*\"] || [];\n var specificTypeHandlers = _clientMeta[this.id] && _clientMeta[this.id].handlers[event.type] || [];\n var handlers = wildcardTypeHandlers.concat(specificTypeHandlers);\n if (handlers && handlers.length) {\n var i, len, func, context, eventCopy, originalContext = this;\n for (i = 0, len = handlers.length; i < len; i++) {\n func = handlers[i];\n context = originalContext;\n if (typeof func === \"string\" && typeof _window[func] === \"function\") {\n func = _window[func];\n }\n if (typeof func === \"object\" && func && typeof func.handleEvent === \"function\") {\n context = func;\n func = func.handleEvent;\n }\n if (typeof func === \"function\") {\n eventCopy = _extend({}, event);\n _dispatchCallback(func, context, [ eventCopy ], async);\n }\n }\n }\n return this;\n };\n /**\n * Prepares the elements for clipping/unclipping.\n *\n * @returns An Array of elements.\n * @private\n */\n var _prepClip = function(elements) {\n if (typeof elements === \"string\") {\n elements = [];\n }\n return typeof elements.length !== \"number\" ? [ elements ] : elements;\n };\n /**\n * Add a `mouseover` handler function for a clipped element.\n *\n * @returns `undefined`\n * @private\n */\n var _addMouseHandlers = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n var _suppressMouseEvents = function(event) {\n if (!(event || (event = _window.event))) {\n return;\n }\n if (event._source !== \"js\") {\n event.stopImmediatePropagation();\n event.preventDefault();\n }\n delete event._source;\n };\n var _elementMouseOver = function(event) {\n if (!(event || (event = _window.event))) {\n return;\n }\n _suppressMouseEvents(event);\n ZeroClipboard.focus(element);\n };\n element.addEventListener(\"mouseover\", _elementMouseOver, false);\n element.addEventListener(\"mouseout\", _suppressMouseEvents, false);\n element.addEventListener(\"mouseenter\", _suppressMouseEvents, false);\n element.addEventListener(\"mouseleave\", _suppressMouseEvents, false);\n element.addEventListener(\"mousemove\", _suppressMouseEvents, false);\n _mouseHandlers[element.zcClippingId] = {\n mouseover: _elementMouseOver,\n mouseout: _suppressMouseEvents,\n mouseenter: _suppressMouseEvents,\n mouseleave: _suppressMouseEvents,\n mousemove: _suppressMouseEvents\n };\n };\n /**\n * Remove a `mouseover` handler function for a clipped element.\n *\n * @returns `undefined`\n * @private\n */\n var _removeMouseHandlers = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n var mouseHandlers = _mouseHandlers[element.zcClippingId];\n if (!(typeof mouseHandlers === \"object\" && mouseHandlers)) {\n return;\n }\n var key, val, mouseEvents = [ \"move\", \"leave\", \"enter\", \"out\", \"over\" ];\n for (var i = 0, len = mouseEvents.length; i < len; i++) {\n key = \"mouse\" + mouseEvents[i];\n val = mouseHandlers[key];\n if (typeof val === \"function\") {\n element.removeEventListener(key, val, false);\n }\n }\n delete _mouseHandlers[element.zcClippingId];\n };\n /**\n * Creates a new ZeroClipboard client instance.\n * Optionally, auto-`clip` an element or collection of elements.\n *\n * @constructor\n */\n ZeroClipboard._createClient = function() {\n _clientConstructor.apply(this, _args(arguments));\n };\n /**\n * Register an event listener to the client.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.on = function() {\n return _clientOn.apply(this, _args(arguments));\n };\n /**\n * Unregister an event handler from the client.\n * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`.\n * If no `eventType` is provided, it will unregister all handlers for every event type.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.off = function() {\n return _clientOff.apply(this, _args(arguments));\n };\n /**\n * Retrieve event listeners for an `eventType` from the client.\n * If no `eventType` is provided, it will retrieve all listeners for every event type.\n *\n * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`\n */\n ZeroClipboard.prototype.handlers = function() {\n return _clientListeners.apply(this, _args(arguments));\n };\n /**\n * Event emission receiver from the Flash object for this client's registered JavaScript event listeners.\n *\n * @returns For the \"copy\" event, returns the Flash-friendly \"clipData\" object; otherwise `undefined`.\n */\n ZeroClipboard.prototype.emit = function() {\n return _clientEmit.apply(this, _args(arguments));\n };\n /**\n * Register clipboard actions for new element(s) to the client.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.clip = function() {\n return _clientClip.apply(this, _args(arguments));\n };\n /**\n * Unregister the clipboard actions of previously registered element(s) on the page.\n * If no elements are provided, ALL registered elements will be unregistered.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.unclip = function() {\n return _clientUnclip.apply(this, _args(arguments));\n };\n /**\n * Get all of the elements to which this client is clipped.\n *\n * @returns array of clipped elements\n */\n ZeroClipboard.prototype.elements = function() {\n return _clientElements.apply(this, _args(arguments));\n };\n /**\n * Self-destruct and clean up everything for a single client.\n * This will NOT destroy the embedded Flash object.\n *\n * @returns `undefined`\n */\n ZeroClipboard.prototype.destroy = function() {\n return _clientDestroy.apply(this, _args(arguments));\n };\n /**\n * Stores the pending plain text to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setText = function(text) {\n ZeroClipboard.setData(\"text/plain\", text);\n return this;\n };\n /**\n * Stores the pending HTML text to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setHtml = function(html) {\n ZeroClipboard.setData(\"text/html\", html);\n return this;\n };\n /**\n * Stores the pending rich text (RTF) to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setRichText = function(richText) {\n ZeroClipboard.setData(\"application/rtf\", richText);\n return this;\n };\n /**\n * Stores the pending data to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setData = function() {\n ZeroClipboard.setData.apply(this, _args(arguments));\n return this;\n };\n /**\n * Clears the pending data to inject into the clipboard.\n * If no `format` is provided, all pending data formats will be cleared.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.clearData = function() {\n ZeroClipboard.clearData.apply(this, _args(arguments));\n return this;\n };\n /**\n * Gets a copy of the pending data to inject into the clipboard.\n * If no `format` is provided, a copy of ALL pending data formats will be returned.\n *\n * @returns `String` or `Object`\n */\n ZeroClipboard.prototype.getData = function() {\n return ZeroClipboard.getData.apply(this, _args(arguments));\n };\n if (typeof define === \"function\" && define.amd) {\n define(function() {\n return ZeroClipboard;\n });\n } else if (typeof module === \"object\" && module && typeof module.exports === \"object\" && module.exports) {\n module.exports = ZeroClipboard;\n } else {\n window.ZeroClipboard = ZeroClipboard;\n }\n})(function() {\n return this || window;\n}());"]}
\ No newline at end of file diff --git a/addons/website_links/static/src/js/ZeroClipboard.swf b/addons/website_links/static/src/js/ZeroClipboard.swf Binary files differnew file mode 100755 index 00000000000..d4e2561b366 --- /dev/null +++ b/addons/website_links/static/src/js/ZeroClipboard.swf diff --git a/addons/website_links/static/src/js/website_links.js b/addons/website_links/static/src/js/website_links.js new file mode 100644 index 00000000000..1c901af0992 --- /dev/null +++ b/addons/website_links/static/src/js/website_links.js @@ -0,0 +1,377 @@ +(function () { + 'use strict'; + +openerp.website.if_dom_contains('div.o_website_links_create_tracked_url', function() { + + var QWeb = openerp.qweb; + var _t = openerp._t; + + openerp.website_links = {}; + + openerp.website_links.SelectBox = openerp.Widget.extend({ + init: function(obj) { + this.obj = openerp.website.session.model(obj); + }, + start: function(element, placeholder) { + var self = this; + this.element = element; + this.placeholder = placeholder; + + this.fetch_objects().then(function(results) { + self.objects = results; + + element.select2({ + placeholder: self.placeholder, + allowClear: true, + createSearchChoice:function(term, data) { + if(self.object_exists(term)) { return null; } + + return {id:term, text:_.str.sprintf("Create '%s'", term)}; + }, + createSearchChoicePosition: 'bottom', + multiple: false, + data: self.objects, + }); + + element.on('change', function(e) { + self.on_change(e); + }); + }); + }, + fetch_objects: function() { + return this.obj.call('search_read', [[]]).then(function(result) { + return _.map(result, function(val) { + return {id: val.id, text:val.name}; + }) + }); + }, + object_exists: function(query) { + return _.find(this.objects, function(val) { + return val.text.toLowerCase() == query.toLowerCase(); + }) != undefined; + }, + on_change: function(e) { + if(e.added && _.isString(e.added.id)) { + this.create_object(e.added.id); + } + }, + create_object: function(name) { + var self = this; + + return this.obj.call('create', [{name:name}]).then(function(record) { + self.element.attr('value', record); + }) + }, + }); + + openerp.website_links.RecentLinkBox = openerp.Widget.extend({ + template: 'website_links.RecentLink', + events: { + 'click .btn_shorten_url_clipboard':'toggle_copy_button', + 'click .o_website_links_edit_code':'edit_code', + 'click .o_website_links_ok_edit':function(e) { + e.preventDefault(); + this.submit_code(); + }, + 'click .o_website_links_cancel_edit':function(e) { + e.preventDefault(); + this.cancel_edit(); + }, + 'submit #o_website_links_edit_code_form':function(e) { + e.preventDefault(); + this.submit_code(); + }, + }, + init: function(parent, link_obj) { + this._super(parent); + this.link_obj = link_obj; + this.animating_copy = false; + }, + start: function() { + new ZeroClipboard(this.$('.btn_shorten_url_clipboard')); + }, + toggle_copy_button: function() { + var self = this; + + if(!this.animating_copy) { + this.animating_copy = true; + var top = this.$('.o_website_links_short_url').position().top; + this.$('.o_website_links_short_url').clone() + .css('position', 'absolute') + .css('left', 15) + .css('top', top-2) + .css('z-index', 2) + .removeClass('o_website_links_short_url') + .addClass('animated-link') + .insertAfter(this.$('.o_website_links_short_url')) + .animate({ + opacity: 0, + top: "-=20", + }, 500, function() { + self.$('.animated-link').remove(); + self.animating_copy = false; + }); + } + }, + remove: function() { + this.getParent().remove_link(this); + }, + notification: function(message) { + this.$('.notification').append('<strong>' + message + '</strong>'); + }, + edit_code: function() { + var init_code = this.$('#o_website_links_code').html(); + + this.$('#o_website_links_code').html("<form style='display:inline;' id='o_website_links_edit_code_form'><input type='hidden' id='init_code' value='" + init_code + "'/><input type='text' id='new_code' value='" + init_code + "'/></form>"); + this.$('.o_website_links_edit_code').hide(); + this.$('.copy-to-clipboard').hide(); + this.$('.o_website_links_edit_tools').show(); + }, + cancel_edit: function() { + this.$('.o_website_links_edit_code').show(); + this.$('.copy-to-clipboard').show(); + this.$('.o_website_links_edit_tools').hide(); + self.$('.o_website_links_code_error').hide(); + + var old_code = this.$('#o_website_links_edit_code_form #init_code').val(); + this.$('#o_website_links_code').html(old_code); + + this.$('#code-error').remove(); + this.$('#o_website_links_code form').remove(); + }, + submit_code: function() { + var self = this; + + var init_code = this.$('#o_website_links_edit_code_form #init_code').val(); + var new_code = this.$('#o_website_links_edit_code_form #new_code').val(); + + function show_new_code(new_code) { + self.$('.o_website_links_code_error').html(''); + self.$('.o_website_links_code_error').hide(); + + self.$('#o_website_links_code form').remove(); + + // Show new code + var host = self.$('#o_website_links_host').html(); + self.$('#o_website_links_code').html(new_code); + + // Update button copy to clipboard + self.$('.btn_shorten_url_clipboard').attr('data-clipboard-text', host + new_code) + + // Show action again + self.$('.o_website_links_edit_code').show(); + self.$('.copy-to-clipboard').show(); + self.$('.o_website_links_edit_tools').hide(); + } + + if(init_code == new_code) { + show_new_code(new_code); + } + else { + openerp.jsonRpc('/website_links/add_code', 'call', {'init_code':init_code, 'new_code':new_code}) + .then(function(result) { + show_new_code(result[0].code); + }) + .fail(function(error) { + self.$('.o_website_links_code_error').show(); + self.$('.o_website_links_code_error').html("This code is already taken"); + }) ; + } + }, + }); + + openerp.website_links.RecentLinks = openerp.Widget.extend({ + init: function($element) { + this._super(); + }, + get_recent_links: function(filter) { + var self = this; + var nb_links = this.getChildren().length; + + openerp.jsonRpc('/website_links/recent_links', 'call', {'filter':filter, 'limit':20}) + .then(function(result) { + _.each(result.reverse(), function(link) { + self.add_link(link); + }); + + self.update_notification(); + }) + .fail(function() { + var message = _t("Unable to get recent links"); + self.$el.append("<div class='alert alert-danger'>" + message + "</div>"); + }); + }, + add_link: function(link) { + var nb_links = this.getChildren().length; + + var recent_link_box = new openerp.website_links.RecentLinkBox(this, link); + recent_link_box.prependTo(this.$el); + $('.link-tooltip').tooltip(); + + if(nb_links == 0) { + this.update_notification(); + } + }, + remove_links: function() { + _.invoke(this.getChildren(), 'remove'); + }, + remove_link: function(link) { + link.destroy(); + }, + update_notification: function() { + if(this.getChildren().length == 0) { + var message = _t("You don't have any recent links."); + $('.o_website_links_recent_links_notification').html("<div class='alert alert-info'>" + message + "</div>"); + } + else { + $('.o_website_links_recent_links_notification').empty(); + } + }, + }); + + openerp.website.ready().done(function() { + + ZeroClipboard.config({swfPath: location.origin + "/website_links/static/src/js/ZeroClipboard.swf" }); + + // UTMS selects widgets + var campaign_select = new openerp.website_links.SelectBox('utm.campaign'); + campaign_select.start($("#campaign-select"), 'e.g. Promotion of June, Winter Newsletter, ..'); + + var medium_select = new openerp.website_links.SelectBox('utm.medium'); + medium_select.start($("#channel-select"), 'e.g. Newsletter, Social Network, ..'); + + var source_select = new openerp.website_links.SelectBox('utm.source'); + source_select.start($("#source-select"), 'e.g. Search Engine, Website page, ..'); + + // Recent Links Widgets + var recent_links; + openerp.website.add_template_file('/website_links/static/src/xml/recent_link.xml') + .then(function() { + recent_links = new openerp.website_links.RecentLinks(); + recent_links.appendTo($("#o_website_links_recent_links")); + recent_links.get_recent_links('newest'); + + $('#filter-newest-links').click(function() { + recent_links.remove_links(); + recent_links.get_recent_links('newest'); + }); + + $('#filter-most-clicked-links').click(function() { + recent_links.remove_links(); + recent_links.get_recent_links('most-clicked'); + }); + + $('#filter-recently-used-links').click(function() { + recent_links.remove_links(); + recent_links.get_recent_links('recently-used'); + }); + }); + + // Clipboard Library + var client = new ZeroClipboard($("#btn_shorten_url")); + + $("#generated_tracked_link a").click(function() { + $("#generated_tracked_link a").text("Copied").removeClass("btn-primary").addClass("btn-success"); + setTimeout(function() { + $("#generated_tracked_link a").text("Copy").removeClass("btn-success").addClass("btn-primary"); + }, '5000'); + }); + + $('#url').on('keyup', function(e) { + if($('#btn_shorten_url').hasClass('btn-copy') && e.which != 13) { + $('#btn_shorten_url').removeClass('btn-success btn-copy').addClass('btn-primary').html('Get tracked link'); + $('#generated_tracked_link').css('display', 'none'); + } + }); + + var url_copy_animating = false; + $('#btn_shorten_url').click(function() { + if($('#btn_shorten_url').hasClass('btn-copy')) { + if(!url_copy_animating) { + url_copy_animating = true; + + $('#generated_tracked_link').clone() + .css('position', 'absolute') + .css('left', '78px') + .css('bottom', '8px') + .css('z-index', 2) + .removeClass('#generated_tracked_link') + .addClass('url-animated-link') + .appendTo($('#generated_tracked_link')) + .animate({ + opacity: 0, + bottom: "+=20", + }, 500, function() { + $('.url-animated-link').remove(); + url_copy_animating = false; + }); + } + } + }) + + // Add the RecentLinkBox widget and send the form when the user generate the link + $("#o_website_links_link_tracker_form").submit(function(event) { + + if($('#btn_shorten_url').hasClass('btn-copy')) { + event.preventDefault(); + return; + } + + event.preventDefault(); + event.stopPropagation(); + + // Get URL and UTMs + var url = $("#url").val(); + var campaign_id = $('#campaign-select').attr('value'); + var medium_id = $('#channel-select').attr('value'); + var source_id = $('#source-select').attr('value'); + + var params = {}; + params.url = $("#url").val(); + if(campaign_id != '') { params.campaign_id = campaign_id; } + if(medium_id != '') { params.medium_id = medium_id; } + if(source_id != '') { params.source_id = source_id; } + + $('#btn_shorten_url').text('Generating link...'); + + openerp.jsonRpc("/website_links/new", 'call', params) + .then(function (result) { + if('error' in result) { + // Handle errors + if(result['error'] == 'empty_url') { + $('.notification').html("<div class='alert alert-danger'>The URL is empty.</div>"); + } + else if(result['error'] == 'url_not_found') { + $('.notification').html("<div class='alert alert-danger'>URL not found (404)</div>"); + } + else { + $('.notification').html("<div class='alert alert-danger'>An error occur while trying to generate your link. Try again later.</div>"); + } + } + else { + // Link generated, clean the form and show the link + var link = result[0]; + + $('#btn_shorten_url').removeClass('btn-primary').addClass('btn-success btn-copy').html('Copy'); + $('#btn_shorten_url').attr('data-clipboard-text', link.short_url); + + $('.notification').html(''); + $('#generated_tracked_link').html(link.short_url); + $('#generated_tracked_link').css('display', 'inline'); + + recent_links.add_link(link); + + // Clean URL and UTM selects + $('#campaign-select').select2('val', ''); + $('#channel-select').select2('val', ''); + $('#source-select').select2('val', ''); + } + }); + }); + + $(function () { + $('[data-toggle="tooltip"]').tooltip() + }) + }); +}); +})();
\ No newline at end of file diff --git a/addons/website_links/static/src/js/website_links_charts.js b/addons/website_links/static/src/js/website_links_charts.js new file mode 100644 index 00000000000..f87a08135bc --- /dev/null +++ b/addons/website_links/static/src/js/website_links_charts.js @@ -0,0 +1,249 @@ +(function () { + 'use strict'; + +openerp.website.if_dom_contains('div.o_website_links_chart', function() { + + var QWeb = openerp.qweb; + var website = openerp.website; + + openerp.website_links_chart = {}; + + openerp.website_links_chart.BarChart = openerp.Widget.extend({ + init: function($element, begin_date, end_date, dates) { + this.$element = $element; + this.begin_date = begin_date; + this.end_date = end_date; + this.number_of_days = this.end_date.diff(this.begin_date, 'days') + 2; + this.dates = dates; + }, + start: function() { + var self = this; + + // Accessor functions + function getDate(d) { return new Date(d[0]); } + function getNbClicks(d) { return d[1]; } + + // Prune tick values for visibility purpose + function getPrunedTickValues(ticks, nb_desired_ticks) { + var nb_values = ticks.length; + var keep_one_of = Math.max(1, Math.floor(nb_values / nb_desired_ticks)); + + return _.filter(ticks, function(d, i) { + return i % keep_one_of == 0; + }); + } + + // Fill data for each day (with 0 click for days without data) + var clicks_array = []; + var begin_date_copy = this.begin_date; + for(var i = 0 ; i < this.number_of_days ; i++) { + var date_key = begin_date_copy.format('YYYY-MM-DD'); + clicks_array.push([date_key, (date_key in this.dates) ? this.dates[date_key] : 0]); + begin_date_copy.add(1, 'days'); + } + + // Set title + var nb_clicks = _.reduce(clicks_array, function(total, val) { return total + val[1] ; }, 0); + $(this.$element + ' .title').html(nb_clicks + ' clicks'); + + // Fit data into the NVD3 scheme + var chart_data = [{}]; + chart_data[0]['key'] = '# of clicks'; + chart_data[0]['values'] = clicks_array; + + nv.addGraph(function() { + var chart = nv.models.lineChart() + .x(function(d) { return getDate(d); }) + .y(function(d) { return getNbClicks(d); }) + .tooltips(true) + .transitionDuration(0) + .showYAxis(true) + .showXAxis(true); + + // Reduce the number of labels on the X axis for visibility + var tick_values = getPrunedTickValues(chart_data[0]['values'], 10); + + chart.xAxis + .tickFormat(function(d) { return d3.time.format("%d/%m/%y")(new Date(d)); }) + .tickValues(_.map(tick_values, function(d) { return getDate(d).getTime(); })) + .rotateLabels(55); + + chart.yAxis + .tickFormat(d3.format("d")) + .ticks(chart_data[0]['values'].length - 1) + + d3.select(self.$element + ' svg') + .datum(chart_data) + .call(chart); + + nv.utils.windowResize(chart.update); + + return chart; + }); + }, + }); + + openerp.website_links_chart.PieChart = openerp.Widget.extend({ + init: function($element, data) { + this.data = data; + this.$element = $element; + }, + start: function() { + var self = this; + + // Process country data to fit into the NVD3 scheme + var processed_data = []; + for(var i = 0 ; i < this.data.length ; i++) { + var country_name = this.data[i]['country_id'] ? this.data[i]['country_id'][1] : 'Undefined'; + processed_data.push({'label':country_name + ' (' + this.data[i]['country_id_count'] + ')', 'value':this.data[i]['country_id_count']}); + } + + // Set title + $(this.$element + ' .title').html(this.data.length + ' countries'); + + nv.addGraph(function() { + var chart = nv.models.pieChart() + .x(function(d) { return d.label }) + .y(function(d) { return d.value }) + .showLabels(true); + + d3.select(self.$element + ' svg') + .datum(processed_data) + .transition().duration(1200) + .call(chart); + + nv.utils.windowResize(chart.update); + return chart; + }); + }, + }); + + website.ready().done(function() { + + var self = this; + + // Resize the chart when a tab is opened, because NVD3 automatically reduce the size + // of the chart to 5px width when the bootstrap tab is closed. + $(".graph-tabs li a").click(function (e) { + e.preventDefault(); + $(this).tab('show'); + $(window).trigger('resize'); // Force NVD3 to redraw the chart + }); + + // Get the code of the link + var code = $('#code').val(); + var link_id = $('#link_id').val(); + + var clicks = website.session.model('website.links.click'); + var links_domain = ['link_id', '=', parseInt(link_id)]; + + var total_clicks = function() { + return clicks.call('search_count', [[links_domain]]); + }; + + var clicks_by_day = function() { + return clicks.call('read_group', [[links_domain], ['create_date']], + {'groupby':'create_date:day'}); + }; + + var clicks_by_country = function() { + return clicks.call('read_group', [[links_domain], ['country_id']], + {'groupby':'country_id'}); + }; + + var last_week_clicks_by_country = function() { + var interval = moment().subtract(7, 'days').format("YYYY-MM-DD"); + return clicks.call('read_group', [[links_domain, ['create_date', '>', interval]], ['country_id']], + {'groupby':'country_id'}); + }; + + var last_month_clicks_by_country = function() { + var interval = moment().subtract(30, 'days').format("YYYY-MM-DD"); + return clicks.call('read_group', [[links_domain, ['create_date', '>', interval]], ['country_id']], + {'groupby':'country_id'}); + }; + + $.when(total_clicks(), + clicks_by_day(), + clicks_by_country(), + last_week_clicks_by_country(), + last_month_clicks_by_country()) + .done(function(total_clicks, clicks_by_day, clicks_by_country, last_week_clicks_by_country, last_month_clicks_by_country) { + + if(total_clicks) { + var formatted_clicks_by_day = {}; + var begin_date, end_date; + for(var i = 0 ; i < clicks_by_day.length ; i++) { + var date = moment(clicks_by_day[i]['create_date:day'], "DD MMMM YYYY"); + if (i == 0) { begin_date = date; } + if (i == clicks_by_day.length - 1) { end_date = date; } + formatted_clicks_by_day[date.format("YYYY-MM-DD")] = clicks_by_day[i]['create_date_count']; + } + + // Process all time line chart data + var now = moment(); + + var all_time_chart = new openerp.website_links_chart.BarChart('#all_time_clicks_chart', begin_date, now, formatted_clicks_by_day); + all_time_chart.start(); + + // Process month line chart data + var begin_date = moment().subtract(30, 'days'); + var month_chart = new openerp.website_links_chart.BarChart('#last_month_clicks_chart', begin_date, now, formatted_clicks_by_day); + month_chart.start(); + + // Process week line chart data + var begin_date = moment().subtract(7, 'days'); + var week_chart = new openerp.website_links_chart.BarChart('#last_week_clicks_chart', begin_date, now, formatted_clicks_by_day); + week_chart.start(); + + // Process pie charts + var all_time_pie_chart = new openerp.website_links_chart.PieChart('#all_time_countries_charts', clicks_by_country); + all_time_pie_chart.start(); + + var last_month_pie_chart = new openerp.website_links_chart.PieChart('#last_month_countries_charts', last_month_clicks_by_country); + last_month_pie_chart.start(); + + var last_week_pie_chart = new openerp.website_links_chart.PieChart('#last_week_countries_charts', last_week_clicks_by_country); + last_week_pie_chart.start(); + } + else { + $('#all_time_charts').prepend('There is no data to show'); + $('#last_month_charts').prepend('There is no data to show'); + $('#last_week_charts').prepend('There is no data to show'); + } + }); + + // Copy to clipboard link + ZeroClipboard.config({swfPath: location.origin + "/website_links/static/src/js/ZeroClipboard.swf" }); + new ZeroClipboard($('.copy-to-clipboard')); + + var animating_copy = false; + + $('.copy-to-clipboard').on('click', function(e) { + + e.preventDefault(); + + if(!animating_copy) { + animating_copy = true; + + $('.o_website_links_short_url').clone() + .css('position', 'absolute') + .css('left', '15px') + .css('bottom', '10px') + .css('z-index', 2) + .removeClass('.o_website_links_short_url') + .addClass('animated-link') + .appendTo($('.o_website_links_short_url')) + .animate({ + opacity: 0, + bottom: "+=20", + }, 500, function() { + $('.animated-link').remove(); + animating_copy = false; + }); + } + }); + }); +}); + +})();
\ No newline at end of file diff --git a/addons/website_links/static/src/js/website_links_code_editor.js b/addons/website_links/static/src/js/website_links_code_editor.js new file mode 100644 index 00000000000..62d4065e20b --- /dev/null +++ b/addons/website_links/static/src/js/website_links_code_editor.js @@ -0,0 +1,83 @@ +(function () { +'use strict'; + +openerp.website.ready().done(function() { +openerp.website.if_dom_contains('.o_website_links_edit_code', function() { + + // Edit the short URL code + $('.o_website_links_edit_code').on('click', function(e) { + e.preventDefault(); + + var init_code = $('#o_website_links_code').html(); + + $('#o_website_links_code').html("<form style='display:inline;' id='edit-code-form'><input type='hidden' id='init_code' value='" + init_code + "'/><input type='text' id='new_code' value='" + init_code + "'/></form>"); + $('.o_website_links_edit_code').hide(); + $('.copy-to-clipboard').hide(); + $('.o_website_links_edit_tools').show(); + + $('.o_website_links_cancel_edit').on('click', function(e) { + e.preventDefault(); + + $('.o_website_links_edit_code').show(); + $('.copy-to-clipboard').show(); + $('.o_website_links_edit_tools').hide(); + $('.o_website_links_code_error').hide(); + + var old_code = $('#edit-code-form #init_code').val(); + $('#o_website_links_code').html(old_code); + + $('#code-error').remove(); + $('#o_website_links_code form').remove(); + }); + + function submit_code() { + var init_code = $('#edit-code-form #init_code').val(); + var new_code = $('#edit-code-form #new_code').val(); + + function show_new_code(new_code) { + $('.o_website_links_code_error').html(''); + $('.o_website_links_code_error').hide(); + + $('#o_website_links_code form').remove(); + + // Show new code + var host = $('#short-url-host').html(); + $('#o_website_links_code').html(new_code); + + // Update button copy to clipboard + $('.copy-to-clipboard').attr('data-clipboard-text', host + new_code) + + // Show action again + $('.o_website_links_edit_code').show(); + $('.copy-to-clipboard').show(); + $('.o_website_links_edit_tools').hide(); + } + + if(init_code == new_code) { + show_new_code(new_code); + } + else { + openerp.jsonRpc('/website_links/add_code', 'call', {'init_code':init_code, 'new_code':new_code}) + .then(function(result) { + show_new_code(result[0].code); + }) + .fail(function(error) { + $('.o_website_links_code_error').show(); + $('.o_website_links_code_error').html("This code is already taken"); + }) ; + } + }; + + $('#edit-code-form').submit(function(e) { + e.preventDefault(); + submit_code(); + }); + + $('.o_website_links_ok_edit').click(function(e) { + e.preventDefault(); + submit_code(); + }); + }); +}); +}); +})();
\ No newline at end of file diff --git a/addons/website_links/static/src/js/website_links_menu.js b/addons/website_links/static/src/js/website_links_menu.js new file mode 100644 index 00000000000..6ef30e93314 --- /dev/null +++ b/addons/website_links/static/src/js/website_links_menu.js @@ -0,0 +1,12 @@ +/* The purpose of this script is to copy the current URL of the website + * into the URL form of the URL shortener (module website_links) + * when the user click the link "Share this page" on top of the page. +*/ + +(function () { + 'use strict'; + + $(document).ready(function () { + $('#o_website_links_share_page').attr('href', '/r?u=' + encodeURIComponent(window.location.href)); + }); +})();
\ No newline at end of file diff --git a/addons/website_links/static/src/xml/recent_link.xml b/addons/website_links/static/src/xml/recent_link.xml new file mode 100644 index 00000000000..ca8f2df17e4 --- /dev/null +++ b/addons/website_links/static/src/xml/recent_link.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates id="template" xml:space="preserve"> + <t t-name="website_links.RecentLink"> + <div class="row mb16"> + <div class="col-sm-1 col-xs-2 text-center"> + <h4><t t-esc="widget.link_obj.count"/></h4> + <p class="text-muted" style="margin-top: -5px;">clicks</p> + </div> + <div class="col-sm-7 col-xs-7"> + + <h4 class="truncate_text"> + <img t-att-src="widget.link_obj.icon_src"/> + <a class="no-link-style" t-att-href="widget.link_obj.url"><t t-esc="widget.link_obj.title"/></a> + </h4> + <p class="text-muted mb0" style="margin-top: -5px;"> + <span class="o_website_links_short_url text-muted" style="position:relative;"> + <span id="o_website_links_host"><t t-esc="widget.link_obj.short_url_host"/></span><span id="o_website_links_code"><t t-esc="widget.link_obj.code"/></span> + </span> + + <span class="o_website_links_edit_tools" style="display:none;"> + <a class="o_website_links_ok_edit btn btn-xs btn-primary" href="#">ok</a> or + <a class="o_website_links_cancel_edit" href="#">cancel</a> + </span> + + <a class="o_website_links_edit_code"><span class="fa fa-pencil gray"></span></a> + + <br/> + <span class="label label-success"><t t-esc="widget.link_obj.campaign_id[1]"/></span> + <span class="label label-success"><t t-esc="widget.link_obj.medium_id[1]"/></span> + <span class="label label-success"><t t-esc="widget.link_obj.source_id[1]"/></span> + </p> + <p class='o_website_links_code_error' style='color:red;font-weight:bold;'></p> + </div> + + <div class="col-sm-4 col-xs-2"> + <button class="btn btn-info btn_shorten_url_clipboard mt8" t-att-data-clipboard-text="widget.link_obj.short_url">Copy</button> + <a t-attf-href="{{widget.link_obj.short_url}}+" class="btn btn-warning mt8">Stats</a> + </div> + </div> + </t> +</templates>
\ No newline at end of file diff --git a/addons/website_links/views/website_links.xml b/addons/website_links/views/website_links.xml new file mode 100644 index 00000000000..9ea8f83d271 --- /dev/null +++ b/addons/website_links/views/website_links.xml @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + <record model="ir.actions.act_window" id="action_view_click_statistics"> + <field name="name">Statistics of Clicks</field> + <field name="res_model">website.links.click</field> + <field name="view_mode">graph</field> + <field name="domain">[('link_id', 'IN', ids)]</field> + </record> + + <!-- Search --> + <record id="view_website_links_filter" model="ir.ui.view"> + <field name="name">Links</field> + <field name="model">website.links</field> + <field name="arch" type="xml"> + <search string="Tracked Link"> + <field name="title"/> + <field name="url" string="Title and URL" filter_domain="['|', ('title', 'ilike', self), ('url', 'ilike', self)]"/> + </search> + </field> + </record> + + <!-- Website links views --> + <record id="view_website_links_form" model="ir.ui.view"> + <field name="name">website.links.form</field> + <field name="model">website.links</field> + <field name="arch" type="xml"> + <form string="Website Link"> + <sheet> + <div class="oe_button_box pull-right"> + <button type="object" icon="fa-sign-out" name="action_visit_page" + string="Visit Page" class="oe_stat_button"/> + + <button type="object" class="oe_stat_button" name="action_view_statistics" icon="fa-bar-chart"> + <field name="count" string="Clicks" widget="statinfo"/> + </button> + + </div> + <group> + <field name="title"/> + <field name="url"/> + <field name="short_url"/> + </group> + + <group> + <field name="campaign_id"/> + <field name="medium_id"/> + <field name="source_id"/> + </group> + </sheet> + </form> + </field> + </record> + + + <record id="view_website_links_form_stats" model="ir.ui.view"> + <field name="name">website.links.my.form</field> + <field name="model">website.links</field> + <field name="arch" type="xml"> + <form string="Website Link Graph"> + <sheet> + <field name="link_click_ids"> + <graph> + <field name="link_id"/> + <field name="ip"/> + <field name="country_id"/> + </graph> + </field> + </sheet> + </form> + </field> + </record> + + <record model="ir.actions.act_window" id="action_website_links_stats"> + <field name="name">website.links.form.graph.action</field> + <field name="res_model">website.links</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="view_id" ref="view_website_links_form_stats"/> + </record> + + <record id="view_website_links_tree" model="ir.ui.view"> + <field name="name">website.links.tree</field> + <field name="model">website.links</field> + <field name="arch" type="xml"> + <tree string="Website Links"> + <field name="create_date"/> + <field name="title"/> + <field name="url"/> + <field name="short_url"/> + <field name="count"/> + </tree> + </field> + </record> + + <record id="view_website_links_graph" model="ir.ui.view" > + <field name="name">website.links.graph</field> + <field name="model">website.links</field> + <field name="arch" type="xml"> + <graph string="Website Links" type="bar"> + <field name="url" type="row"/> + <field name="count" type="measure"/> + </graph> + </field> + </record> + + <!-- Action for website links--> + <record model="ir.actions.act_window" id="action_website_links"> + <field name="name">Link Tracker</field> + <field name="res_model">website.links</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form,graph</field> + </record> + + <!-- Website links click views --> + <record id="view_website_links_click_form" model="ir.ui.view"> + <field name="name">website.links.click.form</field> + <field name="model">website.links.click</field> + <field name="arch" type="xml"> + <form string="Website Link Clicks"> + <header> + </header> + <sheet> + <group> + <field name="link_id"/> + <field name="ip"/> + <field name="country_id"/> + </group> + </sheet> + </form> + </field> + </record> + + <record id="view_website_links_click_tree" model="ir.ui.view"> + <field name="name">website.links.click.tree</field> + <field name="model">website.links.click</field> + <field name="arch" type="xml"> + <tree string="Website Links Clicks"> + <field name="link_id"/> + <field name="ip"/> + <field name="country_id"/> + </tree> + </field> + </record> + + <record id="view_website_links_click_graph" model="ir.ui.view" > + <field name="name">website.links.click.graph</field> + <field name="model">website.links.click</field> + <field name="arch" type="xml"> + <graph string="Website Link Clicks"> + <field name="link_id"/> + <field name="ip"/> + <field name="country_id"/> + </graph> + </field> + </record> + + <!-- Action for website links click--> + <record model="ir.actions.act_window" id="action_website_links_click"> + <field name="name">Tracked Links</field> + <field name="res_model">website.links.click</field> + <field name="view_type">form</field> + <field name="view_mode">graph,tree,form</field> + </record> + + <menuitem id="menu_url_shortener_main" name="Link Tracker" parent="base.marketing_menu" groups="base.group_no_one"/> + <menuitem id="menu_url_shortener_links" name="Tracked Links" parent="menu_url_shortener_main" action="action_website_links"/> + </data> +</openerp> diff --git a/addons/website_links/views/website_links_graphs.xml b/addons/website_links/views/website_links_graphs.xml new file mode 100644 index 00000000000..4c71baf669e --- /dev/null +++ b/addons/website_links/views/website_links_graphs.xml @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + <template id="assets_website_links_charts" inherit_id="website.assets_frontend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/web/static/lib/moment/moment.js"/> + <script type="text/javascript" src="/website_links/static/src/js/website_links_charts.js"/> + <script type="text/javascript" src="/website_links/static/src/js/ZeroClipboard.js"/> + <link rel="stylesheet" type="text/css" href="/website_links/static/src/css/website_links.css" /> + </xpath> + </template> + + <template id="assets_nvd3"> + <link href="/web_graph/static/src/css/nv.d3.css" rel="stylesheet" type="text/css"></link> + <script src="/web_graph/static/lib/nvd3/d3.v3.js"></script> + <script src="/web_graph/static/lib/nvd3/nv.d3.js"></script> + </template> + + <template id="graphs" name="Link Statistics"> + <t t-call="website.layout"> + <t t-set="head"> + <t t-call-assets="website_links.assets_nvd3"/> + </t> + + <div class="o_website_links_chart"> + <div class="container"> + <div class="row mt8"> + <div class="col-sm-12"> + <ol class="breadcrumb"> + <li><a href="/r">Link Tracker</a></li> + <li class="active"><t t-esc="title"/></li> + </ol> + </div> + </div> + + <input type="hidden" id="code" t-att-value="code" /> + <input type="hidden" id="link_id" t-att-value="id" /> + + <h1 class="page-header mt0"><t t-esc="title"/></h1> + + <div class="row"> + <div class="col-sm-8"> + <div class="row"> + <div class="col-sm-4 col-xs-11"> + <p><strong>Original URL</strong></p> + </div> + <div class="col-sm-8 col-xs-11"> + <p class="truncate_text"><a t-att-href="url"><t t-esc="url"/></a></p> + </div> + </div> + + <div class="row"> + <div class="col-sm-4"> + <p><strong>Tracked Link</strong></p> + </div> + <div class="col-sm-8"> + <p> + <span class="o_website_links_short_url" id="short_url"><span id="short-url-host"><t t-esc="short_url_host"/></span><span id="o_website_links_code"><t t-esc="code"/></span></span> + <span class="o_website_links_edit_tools" style="display:none;"> + <a class="o_website_links_ok_edit btn btn-xs btn-primary" href="#">ok</a> or + <a class="o_website_links_cancel_edit" href="#">cancel</a> + </span> + <a class="o_website_links_edit_code"><span class="fa fa-pencil gray"></span></a> + <a class="copy-to-clipboard" t-att-data-clipboard-text="short_url">copy</a> + + </p> + <p class='o_website_links_code_error' style='color:red;font-weight:bold;display:none'></p> + </div> + </div> + + <div class="row"> + <div class="col-sm-4 col-xs-12"> + <p><strong>Redirected URL</strong></p> + </div> + <div class="col-sm-8 col-xs-11" style="word-wrap:break-word;"> + <t t-esc="redirected_url"/> + </div> + </div> + </div> + <div class="col-sm-4"> + <t t-if="campaign_id"> + <div class="row"> + <div class="col-sm-4"> + <p><strong>Campaign</strong></p> + </div> + <div class="col-sm-8"> + <p><t t-esc="campaign_id[1]"/></p> + </div> + </div> + </t> + + <t t-if="medium_id"> + <div class="row"> + <div class="col-sm-4"> + <p><strong>Medium</strong></p> + </div> + <div class="col-sm-8"> + <p><t t-esc="medium_id[1]"/></p> + </div> + </div> + </t> + + <t t-if="source_id"> + <div class="row"> + <div class="col-sm-4"> + <p><strong>Source</strong></p> + </div> + <div class="col-sm-8"> + <p><t t-esc="source_id[1]"/></p> + </div> + </div> + </t> + </div> + </div> + + <h1 class="page-header">Statistics + <small class="pull-right hidden-xs mt16" id="filters"> + <ul class="nav nav-tabs nav-tabs-inline graph-tabs" role="tablist"> + <li class="active"><a href="#all_time_charts" role="tab" data-toggle="tab">All Time</a></li> + <li><a href="#last_month_charts" role="tab" data-toggle="tab">Last Month</a></li> + <li><a href="#last_week_charts" role="tab" data-toggle="tab">Last Week</a></li> + </ul> + </small> + </h1> + + <div class="row"> + <div class="col-sm-12 mb8"> + <div class="tab-content"> + + <!-- All Time Charts --> + <div class="tab-pane active" id="all_time_charts"> + <div class="row"> + <div class="col-sm-7 website_links_click_chart" id="all_time_clicks_chart"> + <h3 class="title"></h3> + <svg style="height:20em;"></svg> + </div> + <div class="col-sm-5 website_links_click_chart" id="all_time_countries_charts"> + <h3 class="title"></h3> + <svg style="height:20em;"></svg> + </div> + </div> + </div> + + <!-- Last Month Charts --> + <div class="tab-pane" id="last_month_charts"> + <div class="row"> + <div class="col-sm-7 website_links_click_chart" id="last_month_clicks_chart"> + <h3 class="title"></h3> + <svg style="height:20em;"></svg> + </div> + <div class="col-sm-5 website_links_click_chart" id="last_month_countries_charts"> + <h3 class="title"></h3> + <svg style="height:20em;"></svg> + </div> + </div> + </div> + + <!-- Last Week Charts --> + <div class="tab-pane" id="last_week_charts"> + <div class="row"> + <div class="col-sm-7 website_links_click_chart" id="last_week_clicks_chart"> + <h3 class="title"></h3> + <svg style="height:20em;"></svg> + </div> + <div class="col-sm-5 website_links_click_chart" id="last_week_countries_charts"> + <h3 class="title"></h3> + <svg style="height:20em;"></svg> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </t> + </template> + </data> +</openerp>
\ No newline at end of file diff --git a/addons/website_links/views/website_links_template.xml b/addons/website_links/views/website_links_template.xml new file mode 100644 index 00000000000..7b019403ba5 --- /dev/null +++ b/addons/website_links/views/website_links_template.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + <template id="assets_website_links" inherit_id="website.assets_frontend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/website_links/static/src/js/website_links.js"/> + <script type="text/javascript" src="/website_links/static/src/js/ZeroClipboard.js"/> + <link rel="stylesheet" type="text/css" href="/website_links/static/src/css/website_links.css" /> + <script type="text/javascript" src="/website_links/static/src/js/website_links_code_editor.js"/> + </xpath> + </template> + + <template id="share_page_menu" inherit_id="website.user_navbar"> + <xpath expr="//li[@groups='base.group_website_designer']" position="replace"> + <li groups="base.group_website_designer"> + <a id="promote-menu-button" class="dropdown-toggle" data-toggle="dropdown" href="#"> + Promote <span class="caret"></span> + </a> + <ul class="dropdown-menu oe_content_menu" role="menu"> + <li> + <a data-action="promote-current-page" href="#"> + <span title="Promote page on the web">Optimise SEO...</span> + </a> + </li> + <li> + <a href="/r" id="o_website_links_share_page"> + <span title="Track this page to count clicks">Share this Page</span> + </a> + </li> + </ul> + </li> + </xpath> + </template> + + <template id="asset_website_links_menu" inherit_id="website.assets_editor"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/website_links/static/src/js/website_links_menu.js"></script> + </xpath> + </template> + + <template id="create_shorten_url"> + <div class="o_website_links_create_tracked_url"> + <div class="container"> + <h1 class="page-header">Link Tracker</h1> + <div class="notification"></div> + + <div class="row"> + <div class="col-sm-7"> + <form class="form-horizontal" id="o_website_links_link_tracker_form"> + + <div class="form-group" id="url-form-group"> + <label class="col-sm-3 control-label text-left">URL</label> + + <div class="col-sm-9"> + <input type="text" id="url" class="form-control required-form-control" required="True" placeholder="e.g. https://www.odoo.com/page/contactus" t-att-value="u"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Campaign <i class="fa fa-info-circle" data-toggle="tooltip" data-placement="top" title="Defines the context of your link. It might be an event you want to promote or a special promotion."></i></label> + + <div class="col-sm-9"> + <input type="hidden" class="form-control" id="campaign-select"/> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Medium <i class="fa fa-info-circle" data-toggle="tooltip" data-placement="top" title="Defines the medium used to share your link. It might be an email, or a Facebook Ads for instance."></i></label> + + <div class="col-sm-9"> + <input type="hidden" class="form-control" id="channel-select" /> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-3 control-label">Source <i class="fa fa-info-circle" data-toggle="tooltip" data-placement="top" title="Defines the source from which your traffic will come from, Facebook or Twitter for instance."></i></label> + + <div class="col-sm-9"> + <input type="hidden" class="form-control" id="source-select" /> + </div> + </div> + + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-9"> + <button type="submit" class="btn btn-primary" id="btn_shorten_url" data-clipboard-text="">Get tracked link</button> + + <span id="generated_tracked_link" style="display:none;" class="text-muted"></span> + </div> + </div> + </form> + </div> + + <div class="col-sm-offset-1 col-sm-3 hidden-xs"> + <p class="text-muted text-justify">Share this page with a <strong>short link</strong> that includes <strong>analytics trackers</strong>.</p> + <p class="text-muted text-justify">Thoses trackers can be used in Google Analytics to track clicks and visitors, or in Odoo reports to track opportunities and related revenues.</p> + </div> + </div> + + <h2 class="page-header">Your tracked links + <small class="pull-right hidden-xs mt16" id="filters"> + <ul class="nav nav-tabs nav-tabs-inline graph-tabs" role="tablist"> + <li class="active"><a href="#" id="filter-newest-links" role="tab" data-toggle="tab">Newest</a></li> + <li><a href="#" id="filter-most-clicked-links" role="tab" data-toggle="tab">Most Clicked</a></li> + <li><a href="#" id="filter-recently-used-links" role="tab" data-toggle="tab">Recently Used</a></li> + </ul> + </small> + </h2> + + <div id="o_website_links_recent_links"> + <div class="o_website_links_recent_links_notification"></div> + </div> + </div> + </div> + </template> + + <template id="page_shorten_url" name="Link Tracker"> + <t t-call="website.layout"> + <t t-call="website_links.create_shorten_url"/> + </t> + </template> + </data> +</openerp> + |
