From 0fb2419d257a33ac140a0c63f58f0bcf7d7aab81 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 15 Jun 2012 18:11:08 +0200 Subject: [PATCH 01/78] [ADD] reset_password module bzr revid: chs@openerp.com-20120615161108-3nvxx4o8b3ozjxvw --- addons/reset_password/__init__.py | 2 + addons/reset_password/__openerp__.py | 23 ++++ addons/reset_password/controllers.py | 17 +++ addons/reset_password/email_templates.xml | 55 ++++++++ addons/reset_password/res_users.py | 118 ++++++++++++++++++ addons/reset_password/res_users.xml | 45 +++++++ .../static/src/css/reset_password.css | 12 ++ .../static/src/js/reset_password.js | 75 +++++++++++ .../static/src/xml/reset_password.xml | 26 ++++ 9 files changed, 373 insertions(+) create mode 100644 addons/reset_password/__init__.py create mode 100644 addons/reset_password/__openerp__.py create mode 100644 addons/reset_password/controllers.py create mode 100644 addons/reset_password/email_templates.xml create mode 100644 addons/reset_password/res_users.py create mode 100644 addons/reset_password/res_users.xml create mode 100644 addons/reset_password/static/src/css/reset_password.css create mode 100644 addons/reset_password/static/src/js/reset_password.js create mode 100644 addons/reset_password/static/src/xml/reset_password.xml diff --git a/addons/reset_password/__init__.py b/addons/reset_password/__init__.py new file mode 100644 index 00000000000..7055f8145e5 --- /dev/null +++ b/addons/reset_password/__init__.py @@ -0,0 +1,2 @@ +import res_users +import controllers diff --git a/addons/reset_password/__openerp__.py b/addons/reset_password/__openerp__.py new file mode 100644 index 00000000000..6e1d8fc1f50 --- /dev/null +++ b/addons/reset_password/__openerp__.py @@ -0,0 +1,23 @@ +{ + 'name': 'Reset Password', + 'description': 'Allow users to reset their password from the login page', + 'author': 'OpenERP SA', + 'version': '1.0', + 'category': 'Tools', + 'website': 'http://www.openerp.com', + 'installable': True, + 'depends': ['anonymous', 'email_template'], + 'data': [ + 'email_templates.xml', + 'res_users.xml', + ], + 'js': [ + 'static/src/js/reset_password.js', + ], + 'css': [ + 'static/src/css/reset_password.css', + ], + 'qweb': [ + 'static/src/xml/reset_password.xml', + ], +} diff --git a/addons/reset_password/controllers.py b/addons/reset_password/controllers.py new file mode 100644 index 00000000000..0b5d3b5b2b3 --- /dev/null +++ b/addons/reset_password/controllers.py @@ -0,0 +1,17 @@ +import simplejson +import urllib2 +import werkzeug + +from openerp.addons.web.common import http as oeweb + +class ResetPassword(oeweb.Controller): + _cp_path = '/reset_password' + + @oeweb.httprequest + def index(self, req, db, token): + req.session.authenticate(db, 'anonymous', 'anonymous', {}) + url = '/web/webclient/home#client_action=reset_password&token=%s' % (token,) + redirect = werkzeug.utils.redirect(url) + cookie_val = urllib2.quote(simplejson.dumps(req.session_id)) + redirect.set_cookie('instance0|session_id', cookie_val) + return redirect diff --git a/addons/reset_password/email_templates.xml b/addons/reset_password/email_templates.xml new file mode 100644 index 00000000000..fd1ca888779 --- /dev/null +++ b/addons/reset_password/email_templates.xml @@ -0,0 +1,55 @@ + + + + + + Reset Password No User + + ]]> + (set by reset_password module) + Password reset attempt + + + + + Reset Password + + ]]> + (set by reset_password module) + Password reset + + + + Password Changed + + ]]> + (set by reset_password module) + Password chaned + + + + + diff --git a/addons/reset_password/res_users.py b/addons/reset_password/res_users.py new file mode 100644 index 00000000000..0b93a9a37ea --- /dev/null +++ b/addons/reset_password/res_users.py @@ -0,0 +1,118 @@ +import urlparse +import itsdangerous +from openerp.tools import config +from openerp.osv import osv, fields + +TWENTY_FOUR_HOURS = 24 * 60 * 60 + +def serializer(dbname): + key = '%s.%s' % (dbname, config['admin_passwd']) + return itsdangerous.URLSafeTimedSerializer(key) + +def generate_token(dbname, user): + s = serializer(dbname) + return s.dumps((user.id, user.user_email)) + +def valid_token(dbname, token, max_age=TWENTY_FOUR_HOURS): + try: + unsign_token(dbname, token, max_age) + return True + except itsdangerous.BadSignature: + return False + +def unsign_token(dbname, token, max_age=TWENTY_FOUR_HOURS): + # TODO avoid replay by comparing timestamp with last connection date of user ? (need a query) + s = serializer(dbname) + return s.loads(token, max_age) + +class res_users(osv.osv): + _inherit = 'res.users' + + _sql_constraints = [ + ('email_uniq', 'UNIQUE (user_email)', 'You can not have two users with the same email!') + ] + + def _rp_send_email(self, cr, uid, email, tpl_name, res_id, context=None): + model, tpl_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reset_password', tpl_name) + assert model == 'email.template' + + host = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '') + ctx = dict(context or {}, url=host) + + msg_id = self.pool.get(model).send_mail(cr, uid, tpl_id, res_id, force_send=False, context=ctx) + MailMessage = self.pool.get('mail.message') + MailMessage.write(cr, uid, [msg_id], {'email_to': email}, context=context) + MailMessage.send(cr, uid, [msg_id], context=context) + + def _rp_get_link(self, cr, uid, ids, context=None): + assert len(ids) == 1 + user = self.browse(cr, uid, ids[0], context=context) + host = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '') + token = generate_token(cr.dbname, user) + link = urlparse.urljoin(host, '/reset_password?db=%s&token=%s' % (cr.dbname, token)) + return link + + def send_reset_password_request(self, cr, uid, email, context=None): + uid = 1 + ids = self.search(cr, uid, [('user_email', '=', email)], context=context) + assert len(ids) <= 1 + if not ids: + _m, company_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company') + self._rp_send_email(cr, uid, email, 'email_no_user', company_id, context=context) + else: + self._rp_send_email(cr, uid, email, 'email_reset_link', ids[0], context=context) + return True + +res_users() + + +class reset_pw_wizard(osv.TransientModel): + _name = 'reset_password.wizard' + _rec_name = 'pw' + _columns = { + 'pw': fields.char('Password', size=64), + 'cpw': fields.char('Confirm Password', size=64), + 'token': fields.char('Token', size=128), + 'state': fields.selection([(x, x) for x in 'draft done missmatch error'.split()], required=True), + } + _defaults = { + 'state': 'draft', + } + + def create(self, cr, uid, values, context=None): + # NOTE here, invalid values raises exceptions to avoid storing + # sensitive data into the database (which then are available to anyone) + + token = values.get('token') + pw = values.get('pw') + cpw = values.get('cpw') + + if pw != cpw: + raise osv.except_osv('Error', 'Passwords missmatch') + + Users = self.pool.get('res.users') + + try: + user_id, user_email = unsign_token(cr.dbname, token) + except Exception: + raise osv.except_osv('Error', 'Invalid token') + + Users.write(cr, 1, user_id, {'password': pw}, context=context) + Users._rp_send_email(cr, 1, user_email, 'email_password_changed', user_id, context=context) + + values = {'state': 'done'} + + return super(reset_pw_wizard, self).create(cr, uid, values, context) + + def change(self, cr, uid, ids, context=None): + return True + + def onchange_token(self, cr, uid, ids, token, context=None): + if not valid_token(cr.dbname, token): + return {'value': {'state': 'error'}} + return {} + + def onchange_pw(self, cr, uid, ids, pw, cpw, context=None): + if pw != cpw: + return {'value': {'state': 'missmatch'}} + return {'value': {'state': 'draft'}} diff --git a/addons/reset_password/res_users.xml b/addons/reset_password/res_users.xml new file mode 100644 index 00000000000..a60198750ce --- /dev/null +++ b/addons/reset_password/res_users.xml @@ -0,0 +1,45 @@ + + + + + + reset_password.wizard.form + reset_password.wizard + form + +
+ + + + + + +
Passwords missmatch
+
+ + +
  • < Back
  • + + + + From 7c0772d4ed9751cea77c4ff4e39140096ce519f6 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 18 Jun 2012 10:19:29 +0200 Subject: [PATCH 02/78] [FIX] reset_password: typo bzr revid: chs@openerp.com-20120618081929-zwuldfdaa7is1yai --- addons/reset_password/email_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/reset_password/email_templates.xml b/addons/reset_password/email_templates.xml index fd1ca888779..37cd529deab 100644 --- a/addons/reset_password/email_templates.xml +++ b/addons/reset_password/email_templates.xml @@ -43,7 +43,7 @@ Kind Regards. ]]> (set by reset_password module) - Password chaned + Password changed Date: Thu, 19 Jul 2012 14:44:46 +0530 Subject: [PATCH 03/78] [IMP]statusbar widget improve for stages clickable bzr revid: sgo@tinyerp.com-20120719091446-qg93k0j6gqlb8udj --- addons/web/static/src/js/view_form.js | 408 +++++++++++--------------- addons/web/static/src/xml/base.xml | 51 ++-- 2 files changed, 190 insertions(+), 269 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 677b7cf7c4e..b615c9af6cd 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -80,7 +80,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.reload_mutex = new $.Mutex(); this.__clicked_inside = false; this.__blur_timeout = null; - this.rendering_engine = new instance.web.form.FormRenderingEngine(this); + this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template }, destroy: function() { @@ -239,13 +239,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } } }, - /** - * - * @param {Object} [options] - * @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode. - * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record - * @return {$.Deferred} - */ do_show: function (options) { var self = this; options = options || {}; @@ -260,24 +253,23 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } this.$element.show().css('visibility', 'hidden'); this.$element.add(this.$buttons).removeClass('oe_form_dirty'); - - var shown = this.has_been_loaded; - if (options.reload !== false) { - shown = shown.pipe(function() { - if (self.dataset.index === null) { - // null index means we should start a new record - return self.on_button_new(); - } - return self.dataset.read_index(_.keys(self.fields_view.fields), { - context: { 'bin_size': true } + return this.has_been_loaded.pipe(function() { + var result; + if (self.dataset.index === null) { + // null index means we should start a new record + result = self.on_button_new(); + } else { + result = self.dataset.read_index(_.keys(self.fields_view.fields), { + context : { 'bin_size' : true } }).pipe(self.on_record_loaded); - }); - } - return shown.pipe(function() { - if (options.editable) { - self.set({mode: "edit"}); } - self.$element.css('visibility', 'visible'); + result.pipe(function() { + if (options.editable) { + self.set({mode: "edit"}); + } + self.$element.css('visibility', 'visible'); + }); + return result; }); }, do_hide: function () { @@ -338,20 +330,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM self.$element.add(self.$buttons).removeClass('oe_form_dirty'); }); }, - /** - * Loads and sets up the default values for the model as the current - * record - * - * @return {$.Deferred} - */ - load_defaults: function () { - var keys = _.keys(this.fields_view.fields); - if (keys.length) { - return this.dataset.default_get(keys) - .pipe(this.on_record_loaded); - } - return this.on_record_loaded({}); - }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, @@ -622,11 +600,22 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM on_button_new: function() { var self = this; this.set({mode: "edit"}); - return $.when(this.has_been_loaded).pipe(function() { + var def = $.Deferred(); + $.when(this.has_been_loaded).then(function() { if (self.can_be_discarded()) { - return self.load_defaults(); + var keys = _.keys(self.fields_view.fields); + if (keys.length) { + self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() { + def.resolve(); + }); + } else { + self.on_record_loaded({}).then(function() { + def.resolve(); + }); + } } }); + return def.promise(); }, on_button_edit: function() { return this.set({mode: "edit"}); @@ -685,14 +674,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM values = {}, first_invalid_field = null; for (var f in self.fields) { - if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && (!self.datarecord.id || (!f.get("readonly") && f._dirty_flag))) { + } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields @@ -701,9 +689,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } if (form_invalid) { self.set({'display_invalid_fields': true}); - for (var g in self.fields) { - if (!self.fields.hasOwnProperty(g)) { continue; } - self.fields[g]._check_css_flags(); + for (var f in self.fields) { + self.fields[f]._check_css_flags(); } first_invalid_field.focus(); self.on_invalid(); @@ -735,15 +722,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM });}); }, on_invalid: function() { - var warnings = _(this.fields).chain() - .filter(function (f) { return !f.is_valid(); }) - .map(function (f) { - return _.str.sprintf('
  • %s
  • ', - _.escape(f.string)); - }).value(); - warnings.unshift('
      '); - warnings.push('
    '); - this.do_warn("The following fields are invalid :", warnings.join('')); + var msg = "
      "; + _.each(this.fields, function(f) { + if (!f.is_valid()) { + msg += "
    • " + f.string + "
    • "; + } + }); + msg += "
    "; + this.do_warn("The following fields are invalid :", msg); }, on_saved: function(r, success) { if (!r.result) { @@ -821,10 +807,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value_, key) { - if (_.include(blacklist, key)) { + if (_.include(blacklist, key)) return; - } - values[key] = value_.get_value(); + var val = value_.get_value(); + values[key] = val; }); return values; }, @@ -879,9 +865,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return option[0] === value; })[1]; break; - case 'many2one': - displayed = field.get_displayed(); - break; } return { @@ -959,9 +942,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM is_create_mode: function() { return !this.datarecord.id; }, - open_translate_dialog: function(field) { - return this._super(field); - }, }); /** @@ -1005,6 +985,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } }); } + selector = 'form[version!="7.0"] page,form[version!="7.0"]'; }, render_to: function($target) { var self = this; @@ -1138,7 +1119,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (found) return; - var $label = $('