From 9fc4ad99f0736b31ac58e25d7eb7a70c7d3ec7f9 Mon Sep 17 00:00:00 2001 From: Anthony Muschang Date: Wed, 11 Jun 2014 11:36:11 +0200 Subject: [PATCH 01/30] [FIX]mass mailing: performance issue on stats #469 --- addons/mass_mailing/mass_mailing.py | 91 +++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py index e7583d8e739..68edcde9390 100644 --- a/addons/mass_mailing/mass_mailing.py +++ b/addons/mass_mailing/mass_mailing.py @@ -37,15 +37,36 @@ class MassMailingCampaign(osv.Model): def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ - results = dict.fromkeys(ids, False) - for campaign in self.browse(cr, uid, ids, context=context): - results[campaign.id] = { - 'sent': len(campaign.statistics_ids), + results = dict.fromkeys(ids, { + 'sent': 0, + 'delivered': 0, + 'opened': 0, + 'replied': 0, + 'bounced': 0, + }) + cr.execute(""" + SELECT + mass_mailing_id, + COUNT(id) AS sent, + COUNT(CASE WHEN bounced is null THEN 1 ELSE null END) AS delivered, + COUNT(CASE WHEN opened is not null THEN 1 ELSE null END) AS opened, + COUNT(CASE WHEN replied is not null THEN 1 ELSE null END) AS replied , + COUNT(CASE WHEN bounced is not null THEN 1 ELSE null END) AS bounced + FROM + mail_mail_statistics + WHERE + mass_mailing_id IN %s + GROUP BY + mass_mailing_id + """, (tuple(ids), )) + for (campaign_id, sent, delivered, opened, replied, bounced) in cr.fetchall(): + results[campaign_id] = { + 'sent': sent, # delivered: shouldn't be: all mails - (failed + bounced) ? - 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and - 'opened': len([stat for stat in campaign.statistics_ids if stat.opened]), - 'replied': len([stat for stat in campaign.statistics_ids if stat.replied]), - 'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]), + 'delivered': delivered, + 'opened': opened, + 'replied': replied, + 'bounced': bounced, } return results @@ -53,14 +74,14 @@ class MassMailingCampaign(osv.Model): """ Gather data about mass mailings to display them in kanban view as nested kanban views is not possible currently. """ results = dict.fromkeys(ids, '') - for campaign in self.browse(cr, uid, ids, context=context): + for campaign_id in ids: mass_mailing_results = [] - for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]: - mass_mailing_object = {} - for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']: - mass_mailing_object[attr] = getattr(mass_mailing, attr) - mass_mailing_results.append(mass_mailing_object) - results[campaign.id] = mass_mailing_results + mass_mailing_results = self.pool['mail.mass_mailing'].search_read(cr, uid, + domain=[('mass_mailing_campaign_id', '=', campaign_id)], + fields=['name', 'sent', 'delivered', 'opened', 'replied', 'bounced'], + limit=self._kanban_mailing_nbr, + context=context) + results[campaign_id] = mass_mailing_results return results _columns = { @@ -195,15 +216,37 @@ class MassMailing(osv.Model): return res def _get_statistics(self, cr, uid, ids, name, arg, context=None): - """ Compute statistics of the mass mailing campaign """ - results = dict.fromkeys(ids, False) - for mass_mailing in self.browse(cr, uid, ids, context=context): - results[mass_mailing.id] = { - 'sent': len(mass_mailing.statistics_ids), - 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and - 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]), - 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]), - 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]), + """ Compute statistics of the mass mailing """ + results = dict.fromkeys(ids, { + 'sent': 0, + 'delivered': 0, + 'opened': 0, + 'replied': 0, + 'bounced': 0, + }) + cr.execute(""" + SELECT + mass_mailing_id, + COUNT(id) AS sent, + COUNT(CASE WHEN bounced is null THEN 1 ELSE null END) AS delivered, + COUNT(CASE WHEN opened is not null THEN 1 ELSE null END) AS opened, + COUNT(CASE WHEN replied is not null THEN 1 ELSE null END) AS replied , + COUNT(CASE WHEN bounced is not null THEN 1 ELSE null END) AS bounced + FROM + mail_mail_statistics + WHERE + mass_mailing_id IN %s + GROUP BY + mass_mailing_id + """, (tuple(ids), )) + for (campaign_id, sent, delivered, opened, replied, bounced) in cr.fetchall(): + results[campaign_id] = { + 'sent': sent, + # delivered: shouldn't be: all mails - (failed + bounced) ? + 'delivered': delivered, + 'opened': opened, + 'replied': replied, + 'bounced': bounced, } return results From d8a0e3d29a6d64822f703027e34238cdd8e0ea2e Mon Sep 17 00:00:00 2001 From: Nicolas Bessi Date: Fri, 27 Jun 2014 14:19:34 +0200 Subject: [PATCH 02/30] Fix wrong relative import of hr_payroll --- addons/l10n_in_hr_payroll/report/report_payslip_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/l10n_in_hr_payroll/report/report_payslip_details.py b/addons/l10n_in_hr_payroll/report/report_payslip_details.py index 462bb374287..37cb7c2a0b7 100644 --- a/addons/l10n_in_hr_payroll/report/report_payslip_details.py +++ b/addons/l10n_in_hr_payroll/report/report_payslip_details.py @@ -20,7 +20,7 @@ ############################################################################## from openerp.report import report_sxw -from hr_payroll import report +from openerp.addons.hr_payroll import report class payslip_details_report_in(report.report_payslip_details.payslip_details_report): @@ -32,4 +32,4 @@ class payslip_details_report_in(report.report_payslip_details.payslip_details_re report_sxw.report_sxw('report.paylip.details.in', 'hr.payslip', 'l10n_in_hr_payroll/report/report_payslip_details.rml', parser=payslip_details_report_in) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From df40926d2a57c101a3e2d221ecfd08fbb4fea30e Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Fri, 27 Jun 2014 16:47:20 +0200 Subject: [PATCH 03/30] [IMP] res_partner: more fault tolerant name_create In case of invalid format such as 'name email@server' (missing chevrons), the parsing would be failing due to a strict behaviour of getaddresses (returns nameemail@server). With the patch this format is accepted. opw 607312 --- openerp/addons/base/res/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index d43028d68ef..30d79bd7da1 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -559,7 +559,7 @@ class res_partner(osv.osv, format_address): """ Supported syntax: - 'Raoul ': will find name and email address - otherwise: default, everything is set as the name """ - emails = tools.email_split(text) + emails = tools.email_split(text.replace(' ',',')) if emails: email = emails[0] name = text[:text.index(email)].replace('"', '').replace('<', '').strip() From 58ad25d0e3f002a630698bd7627657d27490b40e Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 10:46:30 +0200 Subject: [PATCH 04/30] [FIX] website_event - Check that template exists before to render it --- addons/website_event/controllers/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index 70e7b0fbe91..8b62335b481 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -34,6 +34,7 @@ import time from dateutil.relativedelta import relativedelta from openerp import tools import werkzeug.urls +from werkzeug.exceptions import NotFound try: import GeoIP @@ -175,6 +176,16 @@ class website_event(http.Controller): 'event': event, 'main_object': event } + + if '.' not in page: + page = 'website_event.%s' % page + + try: + request.website.get_template(page) + except ValueError, e: + # page not found + raise NotFound + return request.website.render(page, values) @http.route(['/event/'], type='http', auth="public", website=True, multilang=True) From 2c45b7162ce8d067123c54fa7d191c54fd024739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 30 Jun 2014 11:04:24 +0200 Subject: [PATCH 05/30] [IMP] [BACKPORT] Mailing lists usability improvements: headers related to mailing lists, use of email queue for notification emails (> 50 followers), archive website template imp and fix. Backport of commit 839815f of saas-5 (PR 502). --- addons/mail/mail_followers.py | 4 ++-- addons/mail/mail_group.py | 13 +++++++++++++ addons/mail/mail_mail.py | 10 ++++++++-- addons/mail/mail_thread.py | 10 ++++++++++ addons/website_mail_group/__init__.py | 1 + addons/website_mail_group/controllers/main.py | 16 ++++++++++++++-- addons/website_mail_group/models/__init__.py | 1 + addons/website_mail_group/models/mail_group.py | 18 ++++++++++++++++++ .../views/website_mail_group.xml | 2 +- 9 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 addons/website_mail_group/models/__init__.py create mode 100644 addons/website_mail_group/models/mail_group.py diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index 0985b16f5ac..1e2c6c8d9e6 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -183,7 +183,7 @@ class mail_notification(osv.Model): references = message.parent_id.message_id if message.parent_id else False # create email values - max_recipients = 100 + max_recipients = 50 chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)] email_ids = [] for chunk in chunks: @@ -195,7 +195,7 @@ class mail_notification(osv.Model): 'references': references, } email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)) - if force_send and len(chunks) < 6: # for more than 500 followers, use the queue system + if force_send and len(chunks) < 2: # for more than 50 followers, use the queue system self.pool.get('mail.mail').send(cr, uid, email_ids, context=context) return True diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 4ae47a96774..186787c121a 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -211,3 +211,16 @@ class mail_group(osv.Model): return [] else: return super(mail_group, self).get_suggested_thread(cr, uid, removed_suggested_threads, context) + + def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None): + res = super(mail_group, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context) + group = self.browse(cr, uid, id, context=context) + res.update({ + 'headers': { + 'Precedence': 'list', + } + }) + if group.alias_domain: + res['headers']['List-Id'] = '%s.%s' % (group.alias_name, group.alias_domain) + res['headers']['List-Post'] = '' % (group.alias_name, group.alias_domain) + return res diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 65970265ce3..633b270b754 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -201,12 +201,15 @@ class mail_mail(osv.Model): """ body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) body_alternative = tools.html2plaintext(body) - return { + res = { 'body': body, 'body_alternative': body_alternative, 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context), 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context), } + if mail.model and mail.res_id and self.pool.get(mail.model) and hasattr(self.pool[mail.model], 'message_get_email_values'): + res.update(self.pool[mail.model].message_get_email_values(cr, uid, mail.res_id, mail, context=context)) + return res def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None): """ Sends the selected emails immediately, ignoring their current @@ -254,6 +257,9 @@ class mail_mail(osv.Model): # build an RFC2822 email.message.Message object and send it without queuing res = None for email in email_list: + email_headers = dict(headers) + if email.get('headers'): + email_headers.update(email['headers']) msg = ir_mail_server.build_email( email_from=mail.email_from, email_to=email.get('email_to'), @@ -268,7 +274,7 @@ class mail_mail(osv.Model): object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), subtype='html', subtype_alternative='plain', - headers=headers) + headers=email_headers) res = ir_mail_server.send_email(cr, uid, msg, mail_server_id=mail.mail_server_id.id, context=context) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index c9662f04567..3691d352252 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -690,6 +690,16 @@ class mail_thread(osv.AbstractModel): if record.alias_domain and record.alias_name else False for record in self.browse(cr, SUPERUSER_ID, ids, context=context)] + def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None): + """ Temporary method to create custom notification email values for a given + model and document. This should be better to have a headers field on + the mail.mail model, computed when creating the notification email, but + this cannot be done in a stable version. + + TDE FIXME: rethink this ulgy thing. """ + res = dict() + return res + #------------------------------------------------------ # Mail gateway #------------------------------------------------------ diff --git a/addons/website_mail_group/__init__.py b/addons/website_mail_group/__init__.py index ee5959455ad..9f86759e32b 100644 --- a/addons/website_mail_group/__init__.py +++ b/addons/website_mail_group/__init__.py @@ -1 +1,2 @@ import controllers +import models diff --git a/addons/website_mail_group/controllers/main.py b/addons/website_mail_group/controllers/main.py index a28b808d9f2..59478b37fa3 100644 --- a/addons/website_mail_group/controllers/main.py +++ b/addons/website_mail_group/controllers/main.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import datetime +from dateutil import relativedelta -from openerp import tools +from openerp import tools, SUPERUSER_ID from openerp.addons.web import http from openerp.addons.website.models.website import slug from openerp.addons.web.http import request @@ -28,12 +29,23 @@ class MailGroup(http.Controller): def view(self, **post): cr, uid, context = request.cr, request.uid, request.context group_obj = request.registry.get('mail.group') + mail_message_obj = request.registry.get('mail.message') group_ids = group_obj.search(cr, uid, [('alias_id', '!=', False), ('alias_id.alias_name', '!=', False)], context=context) - values = {'groups': group_obj.browse(cr, uid, group_ids, context)} + groups = group_obj.browse(cr, uid, group_ids, context) + # compute statistics + month_date = datetime.datetime.today() - relativedelta.relativedelta(months=1) + group_data = dict.fromkeys(group_ids, dict()) + for group in groups: + group_data[group.id]['monthly_message_nbr'] = mail_message_obj.search( + cr, SUPERUSER_ID, + [('model', '=', 'mail.group'), ('res_id', '=', group.id), ('date', '>=', month_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT))], + count=True, context=context) + values = {'groups': groups, 'group_data': group_data} return request.website.render('website_mail_group.mail_groups', values) @http.route(["/groups/subscription/"], type='json', auth="user") def subscription(self, group_id=0, action=False, **post): + """ TDE FIXME: seems dead code """ cr, uid, context = request.cr, request.uid, request.context group_obj = request.registry.get('mail.group') if action: diff --git a/addons/website_mail_group/models/__init__.py b/addons/website_mail_group/models/__init__.py new file mode 100644 index 00000000000..ea8be51acde --- /dev/null +++ b/addons/website_mail_group/models/__init__.py @@ -0,0 +1 @@ +import mail_group diff --git a/addons/website_mail_group/models/mail_group.py b/addons/website_mail_group/models/mail_group.py new file mode 100644 index 00000000000..804785b66f9 --- /dev/null +++ b/addons/website_mail_group/models/mail_group.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv + + +class MailGroup(osv.Model): + _inherit = 'mail.group' + + def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None): + res = super(MailGroup, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context) + group = self.browse(cr, uid, id, context=context) + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + res['headers'].update({ + 'List-Archive': '<%s/groups/%s>' % (base_url, group.id), + 'List-Subscribe': '<%s/groups>' % (base_url), + 'List-Unsubscribe': '<%s/groups>' % (base_url), + }) + return res diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml index 1c0ee08edd4..21f046fc70b 100644 --- a/addons/website_mail_group/views/website_mail_group.xml +++ b/addons/website_mail_group/views/website_mail_group.xml @@ -40,7 +40,7 @@
participants
- messages + messages / month
From 4d74d9ef53a6c7b8affc1143227cf5ba37a7dd8b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 30 Jun 2014 11:58:11 +0200 Subject: [PATCH 06/30] [FIX] auth_signup: discard signup values that may overwriting existing info --- addons/auth_signup/res_users.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py index 5a744c21c81..576a778be45 100644 --- a/addons/auth_signup/res_users.py +++ b/addons/auth_signup/res_users.py @@ -201,6 +201,14 @@ class res_users(osv.Model): partner.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False}) partner_user = partner.user_ids and partner.user_ids[0] or False + + # avoid overwriting existing (presumably correct) values with geolocation data + if partner.country_id or partner.zip or partner.city: + values.pop('city', None) + values.pop('country_id', None) + if partner.lang: + values.pop('lang', None) + if partner_user: # user exists, modify it according to values values.pop('login', None) From 7ba41e75e2c5b024a17f2fa14b986bdb0719a6d0 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 12:10:49 +0200 Subject: [PATCH 07/30] [FIX] Website pager - Be sure that page in get param is a digit else use page 1. Need to find which controller add ?page= instead of &page= because google find some url with '?page=1?page=2' --> page = '1?page=2'. According to google, the referrer is the old website. --- addons/website/models/website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 434712933f0..0bfd139c17f 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -250,7 +250,7 @@ class website(osv.osv): # Compute Pager page_count = int(math.ceil(float(total) / step)) - page = max(1, min(int(page), page_count)) + page = max(1, min(int(page if page.isdigit() else 1), page_count)) scope -= 1 pmin = max(page - int(math.floor(scope/2)), 1) From f4733aa8954b3d940ae214204a01913b94772c6f Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 12:27:49 +0200 Subject: [PATCH 08/30] [FIX] website - force to test isdigit on a string else a tb "'int' object has no attribute 'isdigit'" can exist if the page var was right and so already and int --- addons/website/models/website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 0bfd139c17f..d6e47f53c43 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -250,7 +250,7 @@ class website(osv.osv): # Compute Pager page_count = int(math.ceil(float(total) / step)) - page = max(1, min(int(page if page.isdigit() else 1), page_count)) + page = max(1, min(int(page if str(page).isdigit() else 1), page_count)) scope -= 1 pmin = max(page - int(math.floor(scope/2)), 1) From e77594c3c3f15ba27c12dee691a8ec778ab9a8be Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 18:29:20 +0200 Subject: [PATCH 09/30] [IMP] website: backport 5112421a (unslug) Update the regexp to be not case sensitive --- addons/website/models/ir_http.py | 6 ++--- addons/website/models/website.py | 14 +++++++++++- addons/website/tests/test_converter.py | 22 +++++++++++++++++++ .../controllers/main.py | 8 +++---- addons/website_customer/controllers/main.py | 8 +++---- addons/website_membership/controllers/main.py | 8 +++---- 6 files changed, 47 insertions(+), 19 deletions(-) diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 3550d2c6373..fc6de7eefc9 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -10,7 +10,7 @@ import werkzeug.routing import openerp from openerp.addons.base import ir from openerp.addons.base.ir import ir_qweb -from openerp.addons.website.models.website import slug +from openerp.addons.website.models.website import slug, _UNSLUG_RE from openerp.http import request from openerp.osv import orm @@ -194,7 +194,7 @@ class ModelConverter(ir.ir_http.ModelConverter): def __init__(self, url_map, model=False, domain='[]'): super(ModelConverter, self).__init__(url_map, model) self.domain = domain - self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)' + self.regex = _UNSLUG_RE.pattern def to_url(self, value): return slug(value) @@ -203,7 +203,7 @@ class ModelConverter(ir.ir_http.ModelConverter): m = re.match(self.regex, value) _uid = RequestUID(value=value, match=m, converter=self) return request.registry[self.model].browse( - request.cr, _uid, int(m.group(1)), context=request.context) + request.cr, _uid, int(m.group(2)), context=request.context) def generate(self, cr, uid, query=None, args=None, context=None): obj = request.registry[self.model] diff --git a/addons/website/models/website.py b/addons/website/models/website.py index d6e47f53c43..261bbf3bc41 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -98,11 +98,23 @@ def slug(value): else: # assume name_search result tuple id, name = value - slugname = slugify(name or '') + slugname = slugify(name or '').strip().strip('-') if not slugname: return str(id) return "%s-%d" % (slugname, id) + +_UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[a-zA-Z0-9-_]+?\w)-)?(-?\d+)(?=$|/)') + +def unslug(s): + """Extract slug and id from a string. + Always return un 2-tuple (str|None, int|None) + """ + m = _UNSLUG_RE.match(s) + if not m: + return None, None + return m.group(1), int(m.group(2)) + def urlplus(url, params): return werkzeug.Href(url)(params or None) diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py index ca72a361ac2..6a6b046ca93 100644 --- a/addons/website/tests/test_converter.py +++ b/addons/website/tests/test_converter.py @@ -9,10 +9,32 @@ from lxml.builder import E from openerp.tests import common from openerp.addons.base.ir import ir_qweb from openerp.addons.website.models.ir_qweb import html_to_text +from openerp.addons.website.models.website import unslug impl = getDOMImplementation() document = impl.createDocument(None, None, None) +class TestUnslug(unittest2.TestCase): + def test_unslug(self): + tests = { + '': (None, None), + 'foo': (None, None), + 'foo-': (None, None), + '-': (None, None), + 'foo-1': ('foo', 1), + 'foo-bar-1': ('foo-bar', 1), + 'foo--1': ('foo', -1), + '1': (None, 1), + '1-1': ('1', 1), + '--1': (None, None), + 'foo---1': (None, None), + 'foo1': (None, None), + } + + for slug, expected in tests.iteritems(): + self.assertEqual(unslug(slug), expected) + + class TestHTMLToText(unittest2.TestCase): def test_rawstring(self): self.assertEqual( diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index faae5593a9b..a880a83ad54 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import logging -import re import werkzeug _logger = logging.getLogger(__name__) @@ -13,7 +12,7 @@ except ImportError: from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request -from openerp.addons.website.models.website import slug +from openerp.addons.website.models.website import slug, unslug from openerp.tools.translate import _ @@ -146,7 +145,7 @@ class WebsiteCrmPartnerAssign(http.Controller): # Do not use semantic controller due to SUPERUSER_ID @http.route(['/partners/'], type='http', auth="public", website=True, multilang=True) def partners_detail(self, partner_id, partner_name='', **post): - mo = re.search('-([-0-9]+)$', str(partner_id)) + _, partner_id = unslug(partner_id) current_grade, current_country = None, None grade_id = post.get('grade_id') country_id = post.get('country_id') @@ -158,8 +157,7 @@ class WebsiteCrmPartnerAssign(http.Controller): country_ids = request.registry['res.country'].exists(request.cr, request.uid, int(country_id), context=request.context) if country_ids: current_country = request.registry['res.country'].browse(request.cr, request.uid, country_ids[0], context=request.context) - if mo: - partner_id = int(mo.group(1)) + if partner_id: partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) if partner.exists() and partner.website_published: values = { diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index 09db80c8908..810faf8bdf6 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import re - import openerp from openerp import SUPERUSER_ID from openerp.addons.web import http +from openerp.addons.website.models.website import unslug from openerp.tools.translate import _ from openerp.addons.web.http import request import werkzeug.urls @@ -84,9 +83,8 @@ class WebsiteCustomer(http.Controller): # Do not use semantic controller due to SUPERUSER_ID @http.route(['/customers/'], type='http', auth="public", website=True, multilang=True) def partners_detail(self, partner_id, **post): - mo = re.search('-([-0-9]+)$', str(partner_id)) - if mo: - partner_id = int(mo.group(1)) + _, partner_id = unslug(partner_id) + if partner_id: partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) if partner.exists() and partner.website_published: values = {} diff --git a/addons/website_membership/controllers/main.py b/addons/website_membership/controllers/main.py index 24e0ed2dd5b..1ab0e5e36af 100644 --- a/addons/website_membership/controllers/main.py +++ b/addons/website_membership/controllers/main.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import re - from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request +from openerp.addons.website.models.website import unslug from openerp.tools.translate import _ import werkzeug.urls @@ -105,9 +104,8 @@ class WebsiteMembership(http.Controller): # Do not use semantic controller due to SUPERUSER_ID @http.route(['/members/'], type='http', auth="public", website=True, multilang=True) def partners_detail(self, partner_id, **post): - mo = re.search('-([-0-9]+)$', str(partner_id)) - if mo: - partner_id = int(mo.group(1)) + _, partner_id = unslug(partner_id) + if partner_id: partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) if partner.exists() and partner.website_published: values = {} From f825b6043bd138049ee2a107e44596ceba8ef954 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 1 Jul 2014 11:29:22 +0200 Subject: [PATCH 10/30] [FIX] rml: avoid reports ending by zero (opw 608073) Commit b6a7402 (reverted at f8671cb) was almost correct, the PageReset should be added at the end of each stories but only if we have one more stories. The PageReset will force the page count to be reseted at zero which means that last page of report would have been at zero. --- openerp/report/render/rml2pdf/trml2pdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index 70965d2c0cd..9ed1a14a21b 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -999,10 +999,10 @@ class _rml_template(object): story_cnt = 0 for node_story in node_stories: if story_cnt > 0: + # Reset Page Number with new story tag + fis.append(PageReset()) fis.append(platypus.PageBreak()) fis += r.render(node_story) - # Reset Page Number with new story tag - fis.append(PageReset()) story_cnt += 1 try: if self.localcontext and self.localcontext.get('internal_header',False): From 3d3134108d51462474a9323903fe5b549aac0119 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 1 Jul 2014 12:22:28 +0200 Subject: [PATCH 11/30] [FIX] web_linkedin: update master img CDN domain, recently changed --- addons/web_linkedin/web_linkedin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web_linkedin/web_linkedin.py b/addons/web_linkedin/web_linkedin.py index 62883840f2d..5a27993bfbd 100644 --- a/addons/web_linkedin/web_linkedin.py +++ b/addons/web_linkedin/web_linkedin.py @@ -36,7 +36,7 @@ class Binary(openerp.addons.web.http.Controller): _scheme, _netloc, path, params, query, fragment = urlparse(url) # media.linkedin.com is the master domain for LinkedIn media (replicated to CDNs), # so forcing it should always work and prevents abusing this method to load arbitrary URLs - url = urlunparse(('http', 'media.linkedin.com', path, params, query, fragment)) + url = urlunparse(('http', 'media.licdn.com', path, params, query, fragment)) bfile = urllib2.urlopen(url) return base64.b64encode(bfile.read()) From 7721ddc7ad30f73f66cafe4981c8ba56a19cfdfe Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Tue, 1 Jul 2014 14:51:00 +0200 Subject: [PATCH 12/30] [FIX] survey: Nasty brackets... --- addons/survey/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 590d7ae22c8..9ccba440c86 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -360,7 +360,7 @@ class survey_survey(osv.Model): for cell in product(rows.keys(), answers.keys()): res[cell] = 0 for input_line in question.user_input_line_ids: - if input_line.answer_type == 'suggestion' and not(current_filters) or input_line.user_input_id.id in current_filters: + if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters): res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1 result_summary = {'answers': answers, 'rows': rows, 'result': res} From 14fae548639f14647796233ea42b4dddcd8ec85a Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Tue, 1 Jul 2014 15:37:27 +0200 Subject: [PATCH 13/30] [FIX] survey: bug while recording comments --- addons/survey/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 9ccba440c86..3a9145eed0b 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -1140,7 +1140,7 @@ class survey_user_input_line(osv.Model): comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() if comment_answer: - vals.update({'answer_type': 'text', 'value_text': comment_answer}) + vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False}) self.create(cr, uid, vals, context=context) return True From 451b6b9f3a5987778227c3eafd1e0d64833e56b4 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 1 Jul 2014 15:48:53 +0200 Subject: [PATCH 14/30] [FIX] project_issue_sheet: make invoiceable field autofield for worklogs When setting a worklog on a project_issue, the field to_invoice is prefilled with on_change_account_id based on the contract settings. As the field was not present on the list view, the information was lost and every worklog was not written as invoiceable, even if enabled on the contract. opw 609082. --- addons/project_issue_sheet/project_issue_sheet_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/project_issue_sheet/project_issue_sheet_view.xml b/addons/project_issue_sheet/project_issue_sheet_view.xml index fb245a0510a..80304deaab1 100644 --- a/addons/project_issue_sheet/project_issue_sheet_view.xml +++ b/addons/project_issue_sheet/project_issue_sheet_view.xml @@ -31,6 +31,7 @@ + From 0b0c1fbdd6f3a1d07e3f16482e416d9053ec98b1 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Tue, 1 Jul 2014 15:56:50 +0200 Subject: [PATCH 15/30] [FIX] survey: catch exception while quizz mode --- addons/survey/survey.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 3a9145eed0b..1389e63e9e2 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -993,6 +993,8 @@ class survey_user_input_line(osv.Model): def __get_mark(self, cr, uid, value_suggested, context=None): try: mark = self.pool.get('survey.label').browse(cr, uid, int(value_suggested), context=context).quizz_mark + except AttributeError: + mark = 0.0 except KeyError: mark = 0.0 except ValueError: From 581341ce3f91f997a7543fa9385a1764c0c53134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 2 Jul 2014 10:25:53 +0200 Subject: [PATCH 16/30] [FIX] mail: fixed display issue with the like button --- addons/mail/static/src/css/mail.css | 8 +------- addons/mail/static/src/xml/mail.xml | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 50e190c846d..e55948634c5 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -164,15 +164,9 @@ border-radius: 3px; margin: 0px; padding-left: 3px; - padding-right: 15px; + padding-right: 5px; margin-right: 5px; } -.openerp .oe_mail .oe_mail_vote_count .oe_e{ - position: absolute; - bottom: 1px; - right: 2px; - font-size: 26px; -} /* c) Message action icons */ diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 2a4ac8d1bf9..5d3175b40d7 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -355,7 +355,7 @@ - 8 + like From 376cdf36b43381c06066cb0b6e5d0ef4b0c45c3b Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 2 Jul 2014 13:56:11 +0200 Subject: [PATCH 17/30] [FIX] auth_oauth: prevent crash on login screen Empty URLs for OAuth providers do not crash anymore the login screen --- addons/auth_oauth/controllers/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 214fdefe1fb..fe3c8bef905 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -48,7 +48,8 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home): def list_providers(self): try: provider_obj = request.registry.get('auth.oauth.provider') - providers = provider_obj.search_read(request.cr, SUPERUSER_ID, [('enabled', '=', True)]) + providers = provider_obj.search_read(request.cr, SUPERUSER_ID, [('enabled', '=', True), ('auth_endpoint', '!=', False), ('validation_endpoint', '!=', False)]) + # TODO in forwardport: remove conditions on 'auth_endpoint' and 'validation_endpoint' when these fields will be 'required' in model except Exception: providers = [] for provider in providers: From 1c5058c931a6ef55f013ec0c0ca2578ddf75f4a4 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 2 Jul 2014 14:07:52 +0200 Subject: [PATCH 18/30] [FIX] auth_oauth: hide invalid providers from login screen --- addons/auth_oauth/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 62eb2e812cc..8fb7ce92607 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -46,7 +46,7 @@ class OAuthController(oeweb.Controller): registry = RegistryManager.get(dbname) with registry.cursor() as cr: providers = registry.get('auth.oauth.provider') - l = providers.read(cr, SUPERUSER_ID, providers.search(cr, SUPERUSER_ID, [('enabled', '=', True)])) + l = providers.read(cr, SUPERUSER_ID, providers.search(cr, SUPERUSER_ID, [('enabled', '=', True), ('auth_endpoint', '!=', False), ('validation_endpoint', '!=', False)])) except Exception: l = [] return l From e80014eae3a622c1491d3f2905b3b6b9e12c6be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lionel=20Sausin=20=28Num=C3=A9rigraphe=29?= Date: Sat, 31 May 2014 19:00:34 +0200 Subject: [PATCH 19/30] [FIX] Fixes #273: avoid double-warning when changing the quantity of a stock move --- addons/stock/stock.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 981b6f749c0..3b1e70646f1 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1901,7 +1901,6 @@ class stock_move(osv.osv): result = { 'product_qty': 0.00 } - warning = {} if (not product_id) or (product_uos_qty <=0.0): result['product_uos_qty'] = 0.0 @@ -1909,22 +1908,15 @@ class stock_move(osv.osv): product_obj = self.pool.get('product.product') uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff']) - - # Warn if the quantity was decreased - for move in self.read(cr, uid, ids, ['product_uos_qty']): - if product_uos_qty < move['product_uos_qty']: - warning.update({ - 'title': _('Warning: No Back Order'), - 'message': _("By changing the quantity here, you accept the " - "new quantity as complete: OpenERP will not " - "automatically generate a Back Order.") }) - break + + # No warning if the quantity was decreased to avoid double warnings: + # The clients should call onchange_quantity too anyway if product_uos and product_uom and (product_uom != product_uos): result['product_qty'] = product_uos_qty / uos_coeff['uos_coeff'] else: result['product_qty'] = product_uos_qty - return {'value': result, 'warning': warning} + return {'value': result} def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, partner_id=False): From a6bf8cd07bc0712d7466b51f4f346ac91a61d7ea Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 2 Jul 2014 16:25:24 +0200 Subject: [PATCH 20/30] [FIX] website: avoid divisions by zero when no record to display --- addons/website/views/website_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index de4eb4fb94b..5932931688a 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -411,7 +411,7 @@ - +
From fbbc3a54e91e7768ecf33038154a2bdbd87505ba Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 2 Jul 2014 17:29:13 +0200 Subject: [PATCH 21/30] [FIX] RML reporting: understandable crash message When tag is misused in an RML report, logs a more explicit error --- openerp/report/render/rml2pdf/trml2pdf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index 9ed1a14a21b..ec5e9484805 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -373,7 +373,11 @@ class _rml_canvas(object): v = utils.attr_get(node, ['x','y']) text=self._textual(node, **v) text = utils.xml2str(text) - self.canvas.drawString(text=text, **v) + try: + self.canvas.drawString(text=text, **v) + except TypeError as e: + _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") + raise e def _drawCenteredString(self, node): v = utils.attr_get(node, ['x','y']) From 04eff4fe3d157774a1529c743f24b00cb2e28d93 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Thu, 3 Jul 2014 09:36:46 +0200 Subject: [PATCH 22/30] [TYPO] Stupid typo... --- openerp/report/render/rml2pdf/trml2pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index ec5e9484805..75bc30d4a19 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -376,7 +376,7 @@ class _rml_canvas(object): try: self.canvas.drawString(text=text, **v) except TypeError as e: - _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") + _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") raise e def _drawCenteredString(self, node): From bf353998f20c27eed3494a172c7afa174c20865a Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 3 Jul 2014 12:30:48 +0200 Subject: [PATCH 23/30] [FIX] res_partner: backport of rev 37bf72a Correctly take the 'use parent address' into account in the onchange. Slightly updated the view that was weird with this parameter. opw 609344 --- openerp/addons/base/res/res_partner.py | 8 +++++--- openerp/addons/base/res/res_partner_view.xml | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 30d79bd7da1..4d86c1cbf3f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -344,6 +344,7 @@ class res_partner(osv.osv, format_address): value = {} value['title'] = False if is_company: + value['use_parent_address'] = False domain = {'title': [('domain', '=', 'partner')]} else: domain = {'title': [('domain', '=', 'contact')]} @@ -363,9 +364,10 @@ class res_partner(osv.osv, format_address): 'was never correctly set. If an existing contact starts working for a new ' 'company then a new contact should be created under that new ' 'company. You can use the "Discard" button to abandon this change.')} - parent = self.browse(cr, uid, parent_id, context=context) - address_fields = self._address_fields(cr, uid, context=context) - result['value'] = dict((key, value_or_id(parent[key])) for key in address_fields) + if use_parent_address: + parent = self.browse(cr, uid, parent_id, context=context) + address_fields = self._address_fields(cr, uid, context=context) + result['value'] = dict((key, value_or_id(parent[key])) for key in address_fields) else: result['value'] = {'use_parent_address': False} return result diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index fd394549f4b..724cce7bb5f 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -157,8 +157,8 @@
-