From a77f5cf42faa75a2dd3931d83ad8ea86648248c0 Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Tue, 25 Aug 2015 21:50:31 +0200 Subject: [PATCH] [ADD] website_form: generic form controller Contains a new ajax post function that comes from mdo's original code and that I was not able to get rid of. --- addons/web/static/src/js/framework/ajax.js | 47 ++++ addons/website_crm/__init__.py | 2 +- addons/website_crm/__openerp__.py | 6 +- addons/website_crm/controllers/main.py | 109 --------- addons/website_crm/data/website_crm_data.xml | 7 +- addons/website_crm/models/__init__.py | 1 + addons/website_crm/models/crm_lead.py | 8 + .../static/src/js/website_crm_tour.js | 103 +++++++++ addons/website_crm/tests/__init__.py | 1 + addons/website_crm/tests/test_website_crm.py | 7 + addons/website_crm/views/website_crm.xml | 175 +++++++------- addons/website_crm_score/controllers/main.py | 43 ++-- addons/website_form/__init__.py | 2 + addons/website_form/__openerp__.py | 18 ++ .../controllers/__init__.py | 0 addons/website_form/controllers/main.py | 217 ++++++++++++++++++ addons/website_form/models/__init__.py | 1 + addons/website_form/models/models.py | 47 ++++ .../static/src/js/website_form.js | 168 ++++++++++++++ .../static/src/less/website_form.less | 54 +++++ .../static/src/xml/website_form.xml | 25 ++ addons/website_form/views/assets.xml | 17 ++ addons/website_hr_recruitment/__openerp__.py | 2 +- .../controllers/main.py | 37 --- .../data/config_data.xml | 6 +- .../website_hr_recruitment/models/__init__.py | 1 + .../models/hr_applicant.py | 11 + .../src/js/website_hr_recruitment_tour.js | 92 ++++++++ .../website_hr_recruitment/tests/__init__.py | 1 + .../tests/test_website_hr_recruitment.py | 7 + .../views/templates.xml | 106 +++++---- addons/website_issue/__init__.py | 0 addons/website_issue/__openerp__.py | 18 ++ .../website_issue/data/website_issue_data.xml | 15 ++ 34 files changed, 1048 insertions(+), 306 deletions(-) delete mode 100644 addons/website_crm/controllers/main.py create mode 100644 addons/website_crm/models/__init__.py create mode 100644 addons/website_crm/models/crm_lead.py create mode 100644 addons/website_crm/static/src/js/website_crm_tour.js create mode 100644 addons/website_crm/tests/__init__.py create mode 100644 addons/website_crm/tests/test_website_crm.py create mode 100644 addons/website_form/__init__.py create mode 100644 addons/website_form/__openerp__.py rename addons/{website_crm => website_form}/controllers/__init__.py (100%) create mode 100644 addons/website_form/controllers/main.py create mode 100644 addons/website_form/models/__init__.py create mode 100644 addons/website_form/models/models.py create mode 100644 addons/website_form/static/src/js/website_form.js create mode 100644 addons/website_form/static/src/less/website_form.less create mode 100644 addons/website_form/static/src/xml/website_form.xml create mode 100644 addons/website_form/views/assets.xml create mode 100644 addons/website_hr_recruitment/models/hr_applicant.py create mode 100644 addons/website_hr_recruitment/static/src/js/website_hr_recruitment_tour.js create mode 100644 addons/website_hr_recruitment/tests/__init__.py create mode 100644 addons/website_hr_recruitment/tests/test_website_hr_recruitment.py create mode 100644 addons/website_issue/__init__.py create mode 100644 addons/website_issue/__openerp__.py create mode 100644 addons/website_issue/data/website_issue_data.xml diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js index 444784ef0fd..f8392287b26 100644 --- a/addons/web/static/src/js/framework/ajax.js +++ b/addons/web/static/src/js/framework/ajax.js @@ -2,6 +2,7 @@ odoo.define('web.ajax', function (require) { "use strict"; var time = require('web.time'); +var utils = require('web.utils'); function genericJsonRpc (fct_name, params, fct) { var data = { @@ -274,6 +275,51 @@ function get_file(options) { timer = setTimeout(waitLoop, CHECK_INTERVAL); }; +function post (controller_url, data) { + + var progressHandler = function (deferred) { + return function (state) { + if(state.lengthComputable) { + deferred.notify({ + h_loaded: utils.human_size(state.loaded), + h_total : utils.human_size(state.total), + loaded : state.loaded, + total : state.total, + pcent : Math.round((state.loaded/state.total)*100) + }); + } + }; + }; + + var Def = $.Deferred(); + var compatibility = !(typeof(FormData)); + var postData = compatibility ? new FormDataCompatibility() : new FormData(); + + $.each(data, function(i,val) { + postData.append(i, val); + }); + + var xhr = new XMLHttpRequest(); + if(compatibility) { + postData.setContentTypeHeader(xhr); + postData = postData.buildBody(); + } + + if(xhr.upload) xhr.upload.addEventListener('progress', progressHandler(Def), false); + + var ajaxDef = $.ajax(controller_url, { + xhr: function() {return xhr;}, + data: postData, + processData: false, + contentType: false, + type: 'POST' + }).then(function (data) {Def.resolve(data);}) + .fail(function (data) {Def.reject(data);}); + + return Def; +} + + var loadXML = (function () { var loading = false; @@ -315,6 +361,7 @@ return { loadJS: loadJS, loadXML: loadXML, get_file: get_file, + post: post, }; }); diff --git a/addons/website_crm/__init__.py b/addons/website_crm/__init__.py index ee5959455ad..bff786c0885 100644 --- a/addons/website_crm/__init__.py +++ b/addons/website_crm/__init__.py @@ -1 +1 @@ -import controllers +import models diff --git a/addons/website_crm/__openerp__.py b/addons/website_crm/__openerp__.py index b265efde01b..98eb8000673 100644 --- a/addons/website_crm/__openerp__.py +++ b/addons/website_crm/__openerp__.py @@ -3,17 +3,17 @@ 'category': 'Website', 'website': 'https://www.odoo.com/page/website-builder', 'summary': 'Create Leads From Contact Form', - 'version': '1.0', + 'version': '2.0', 'description': """ OpenERP Contact Form ==================== """, - 'depends': ['website_partner', 'crm'], + 'depends': ['website_form','website_partner', 'crm'], 'data': [ 'data/website_crm_data.xml', 'views/website_crm.xml', ], 'installable': True, - 'auto_install': False, + 'auto_install': True, } diff --git a/addons/website_crm/controllers/main.py b/addons/website_crm/controllers/main.py deleted file mode 100644 index e92a2483f37..00000000000 --- a/addons/website_crm/controllers/main.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -import base64 - -import werkzeug -import werkzeug.urls - -from openerp import http, SUPERUSER_ID -from openerp.http import request -from openerp.tools.translate import _ - - -class contactus(http.Controller): - - def generate_google_map_url(self, street, city, city_zip, country_name): - url = "http://maps.googleapis.com/maps/api/staticmap?center=%s&sensor=false&zoom=8&size=298x298" % werkzeug.url_quote_plus( - '%s, %s %s, %s' % (street, city, city_zip, country_name) - ) - return url - - @http.route(['/page/website.contactus', '/page/contactus'], type='http', auth="public", website=True) - def contact(self, **kwargs): - values = {} - for field in ['description', 'partner_name', 'phone', 'contact_name', 'email_from', 'name']: - if kwargs.get(field): - values[field] = kwargs.pop(field) - values.update(kwargs=kwargs.items()) - return request.website.render("website.contactus", values) - - def create_lead(self, request, values, kwargs): - """ Allow to be overrided """ - cr, context = request.cr, request.context - return request.registry['crm.lead'].create(cr, SUPERUSER_ID, values, context=dict(context, mail_create_nosubscribe=True)) - - def preRenderThanks(self, values, kwargs): - """ Allow to be overrided """ - company = request.website.company_id - return { - 'google_map_url': self.generate_google_map_url(company.street, company.city, company.zip, company.country_id and company.country_id.name_get()[0][1] or ''), - '_values': values, - '_kwargs': kwargs, - } - - def get_contactus_response(self, values, kwargs): - values = self.preRenderThanks(values, kwargs) - return request.website.render(kwargs.get("view_callback", "website_crm.contactus_thanks"), values) - - @http.route(['/crm/contactus'], type='http', auth="public", website=True) - def contactus(self, **kwargs): - def dict_to_str(title, dictvar): - ret = "\n\n%s" % title - for field in dictvar: - ret += "\n%s" % field - return ret - - _TECHNICAL = ['show_info', 'view_from', 'view_callback'] # Only use for behavior, don't stock it - _BLACKLIST = ['id', 'create_uid', 'create_date', 'write_uid', 'write_date', 'user_id', 'active'] # Allow in description - _REQUIRED = ['name', 'contact_name', 'email_from', 'description'] # Could be improved including required from model - - post_file = [] # List of file to add to ir_attachment once we have the ID - post_description = [] # Info to add after the message - values = {} - - values['medium_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'crm.crm_medium_website') - - for field_name, field_value in kwargs.items(): - if hasattr(field_value, 'filename'): - post_file.append(field_value) - elif field_name in request.registry['crm.lead']._fields and field_name not in _BLACKLIST: - values[field_name] = field_value - elif field_name not in _TECHNICAL: # allow to add some free fields or blacklisted field like ID - post_description.append("%s: %s" % (field_name, field_value)) - - if "name" not in kwargs and values.get("contact_name"): # if kwarg.name is empty, it's an error, we cannot copy the contact_name - values["name"] = values.get("contact_name") - # fields validation : Check that required field from model crm_lead exists - error = set(field for field in _REQUIRED if not values.get(field)) - - if error: - values = dict(values, error=error, kwargs=kwargs.items()) - return request.website.render(kwargs.get("view_from", "website.contactus"), values) - - # description is required, so it is always already initialized - if post_description: - values['description'] += dict_to_str(_("Custom Fields: "), post_description) - - if kwargs.get("show_info"): - post_description = [] - environ = request.httprequest.headers.environ - post_description.append("%s: %s" % ("IP", environ.get("REMOTE_ADDR"))) - post_description.append("%s: %s" % ("USER_AGENT", environ.get("HTTP_USER_AGENT"))) - post_description.append("%s: %s" % ("ACCEPT_LANGUAGE", environ.get("HTTP_ACCEPT_LANGUAGE"))) - post_description.append("%s: %s" % ("REFERER", environ.get("HTTP_REFERER"))) - values['description'] += dict_to_str(_("Environ Fields: "), post_description) - - lead_id = self.create_lead(request, dict(values, user_id=False), kwargs) - values.update(lead_id=lead_id) - if lead_id: - for field_value in post_file: - attachment_value = { - 'name': field_value.filename, - 'res_name': field_value.filename, - 'res_model': 'crm.lead', - 'res_id': lead_id, - 'datas': base64.encodestring(field_value.read()), - 'datas_fname': field_value.filename, - } - request.registry['ir.attachment'].create(request.cr, SUPERUSER_ID, attachment_value, context=request.context) - - return self.get_contactus_response(values, kwargs) diff --git a/addons/website_crm/data/website_crm_data.xml b/addons/website_crm/data/website_crm_data.xml index 8d6c916fffa..a116a6ae411 100644 --- a/addons/website_crm/data/website_crm_data.xml +++ b/addons/website_crm/data/website_crm_data.xml @@ -1,12 +1,18 @@ + + + True + Create a lead + Website Contact Form self /page/contactus + open @@ -19,6 +25,5 @@ True - diff --git a/addons/website_crm/models/__init__.py b/addons/website_crm/models/__init__.py new file mode 100644 index 00000000000..4970d37d01b --- /dev/null +++ b/addons/website_crm/models/__init__.py @@ -0,0 +1 @@ +import crm_lead diff --git a/addons/website_crm/models/crm_lead.py b/addons/website_crm/models/crm_lead.py new file mode 100644 index 00000000000..ce1fad60cdd --- /dev/null +++ b/addons/website_crm/models/crm_lead.py @@ -0,0 +1,8 @@ +from openerp import models, SUPERUSER_ID + +class Lead(models.Model): + _inherit = 'crm.lead' + + def website_form_input_filter(self, request, values): + values.setdefault('medium_id', request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'crm.crm_medium_website')) + return values diff --git a/addons/website_crm/static/src/js/website_crm_tour.js b/addons/website_crm/static/src/js/website_crm_tour.js new file mode 100644 index 00000000000..e17c0dafac6 --- /dev/null +++ b/addons/website_crm/static/src/js/website_crm_tour.js @@ -0,0 +1,103 @@ +odoo.define('website_crm.tour', function(require) { + 'use strict'; + + var base = require('web_editor.base'); + var core = require('web.core'); + var Tour = require('web.Tour'); + var Model = require('web.Model'); + var Session = require('web.Session'); + + base.ready().done(function () { + Tour.register({ + id: 'website_crm_tour', + name: "Test the contact us form", + path: '/page/contactus', + mode: 'test', + steps: [ + { + title: "Complete name", + element: "input[name=contact_name]", + sampleText: "John Smith", + }, + { + title: "Complete phone number", + element: "input[name=phone]", + sampleText: "118.218" + }, + { + title: "Complete Email", + element: "input[name=email_from]", + sampleText: "john@smith.com" + }, + { + title: "Complete Company", + element: "input[name=partner_name]", + sampleText: "Odoo S.A." + }, + { + title: "Complete Subject", + element: "input[name=name]", + sampleText: "Useless message" + }, + { + title: "Complete Subject", + element: "textarea[name=description]", + sampleText: "Even more useless message" + }, + { + title: "Send the form", + element: ".o_website_form_send" + }, + { + title: "Check we were redirected to the success page", + waitFor: "#wrap:has(h1:contains('Thanks')):has(div.alert-success)" + } + ] + }); + + Tour.register({ + id: 'website_crm_tour_results', + name: "Test the contact us form created records", + path: '', + mode: 'test', + steps: [ + { + title: "Check crm.lead record has been created", + waitFor: ".o-apps", + onload: function (tour) { + var leadDef = new Model("crm.lead").call( + "search_read", + [ + [ + ['contact_name', '=' , 'John Smith'], + ['phone' , '=' , '118.218'], + ['email_from' , '=' , 'john@smith.com'], + ['partner_name', '=' , 'Odoo S.A.'], + ['name' , '=' , 'Useless message'], + ['description' , 'like', 'Even more useless message'] + ], + [] + ] + ); + var success = function(model, data) { + if(data.length) { + $('body').append('
'); + } + }; + leadDef.then(_.bind(success, this, 'crm_lead')); + } + }, + { + title: "Check crm.lead record has been created", + waitFor: "#website_form_success_test_tour_crm_lead" + }, + { + title: "Final Step", + waitFor: "html" + } + ] + }); + }); + + return {}; +}); diff --git a/addons/website_crm/tests/__init__.py b/addons/website_crm/tests/__init__.py new file mode 100644 index 00000000000..7c1af2a6172 --- /dev/null +++ b/addons/website_crm/tests/__init__.py @@ -0,0 +1 @@ +import test_website_crm diff --git a/addons/website_crm/tests/test_website_crm.py b/addons/website_crm/tests/test_website_crm.py new file mode 100644 index 00000000000..ddf627d9fbe --- /dev/null +++ b/addons/website_crm/tests/test_website_crm.py @@ -0,0 +1,7 @@ +import openerp.tests +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) +class TestWebsiteCrm(openerp.tests.HttpCase): + def test_tour(self): + self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('website_crm_tour', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.website_crm_tour", login="admin") + self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('website_crm_tour_results', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.website_crm_tour_results", login="admin") \ No newline at end of file diff --git a/addons/website_crm/views/website_crm.xml b/addons/website_crm/views/website_crm.xml index 124a2ac6cc6..baee365ae01 100644 --- a/addons/website_crm/views/website_crm.xml +++ b/addons/website_crm/views/website_crm.xml @@ -1,95 +1,94 @@ - + + - - - - - - + + + diff --git a/addons/website_crm_score/controllers/main.py b/addons/website_crm_score/controllers/main.py index 3707a8f648f..fe983c98d37 100644 --- a/addons/website_crm_score/controllers/main.py +++ b/addons/website_crm_score/controllers/main.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- +import simplejson from openerp import http, SUPERUSER_ID, fields from openerp.http import request from openerp.tools import html_escape from openerp.addons.website.controllers.main import Website -from openerp.addons.website_crm.controllers.main import contactus +from openerp.addons.website_form.controllers.main import WebsiteForm class PageController(Website): @@ -33,27 +34,31 @@ class PageController(Website): return response -class ContactController(contactus): +class ContactController(WebsiteForm): + + @http.route('/website_form/', type='http', auth="public", methods=['POST'], website=True) + def website_form(self, model_name, **kwargs): + response = super(ContactController, self).website_form(model_name, **kwargs) + if model_name != 'crm.lead': + return response - @http.route(['/crm/contactus'], type='http', auth="public", website=True) - def contactus(self, **kwargs): - response = super(ContactController, self).contactus(**kwargs) # the cookie is written here because the response is not available in the create_lead function - if '_values' in response.qcontext: # contactus error : fields validation not passed - # NOTE: this requires for the lead_id to be added to values in the parent controller - lead_id = response.qcontext.get('_values').get('lead_id') - if lead_id: # a new lead has been created - lead_model = request.registry['crm.lead'] - # sign the lead_id - sign = lead_model.encode(lead_id) - response.set_cookie('lead_id', sign, domain=lead_model.get_score_domain_cookies()) + response_data = simplejson.loads(response.data) # controller is json now + if 'id' in response_data: # a new lead has been created + lead_model = request.registry['crm.lead'] + # sign the lead_id + sign = lead_model.encode(response_data['id']) + response.set_cookie('lead_id', sign, domain=lead_model.get_score_domain_cookies()) return response - def create_real_lead(self, request, values, kwargs): - """ Make this function overridable to create a lead with another function that create_lead from webiste_crm """ - return super(ContactController, self).create_lead(request, values, kwargs) + def create_real_lead(self, request, model, values, custom, meta=None): + """ Make this function overridable to create a lead with another function than insert_record from website_form """ + return super(ContactController, self).insert_record(request, model, values, custom, meta) + + def insert_record(self, request, model, values, custom, meta=None): + if model.model != 'crm.lead': + return super(ContactController, self).insert_record(request, model, values, custom, meta) - def create_lead(self, request, values, kwargs): cr, context = request.cr, request.context lead_model = request.registry["crm.lead"] @@ -91,6 +96,8 @@ class ContactController(contactus): body += '
%s: %s' % (fieldname, html_escape(changed_values[fieldname])) request.registry['crm.lead'].message_post(cr, SUPERUSER_ID, [lead_id], body=body, subject="Field value changed", context=context) + return lead_id + else: # either no lead_id cookie OR the lead_id doesn't exist in db OR the current one is closed -> a lead is created lang = context.get('lang', False) @@ -115,7 +122,7 @@ class ContactController(contactus): urls.append('%s' % (url_encoded, url_encoded)) body = '
'.join(urls) - new_lead_id = self.create_real_lead(request, values, kwargs) + new_lead_id = self.create_real_lead(request, model, values, custom, meta) # if pages were seen, a message is posted if body: diff --git a/addons/website_form/__init__.py b/addons/website_form/__init__.py new file mode 100644 index 00000000000..9f86759e32b --- /dev/null +++ b/addons/website_form/__init__.py @@ -0,0 +1,2 @@ +import controllers +import models diff --git a/addons/website_form/__openerp__.py b/addons/website_form/__openerp__.py new file mode 100644 index 00000000000..7fef0b7fd63 --- /dev/null +++ b/addons/website_form/__openerp__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Website Form Builder', + 'category': 'Website', + 'summary': 'Build custom web forms using the website builder', + 'version': '1.0', + 'description': """ +Odoo Form Builder +==================== + +Allows you to build web forms on the website using the website builder. + """, + 'depends': ['website', 'mail'], + 'data': [ + 'views/assets.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/website_crm/controllers/__init__.py b/addons/website_form/controllers/__init__.py similarity index 100% rename from addons/website_crm/controllers/__init__.py rename to addons/website_form/controllers/__init__.py diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py new file mode 100644 index 00000000000..214a8647e07 --- /dev/null +++ b/addons/website_form/controllers/main.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +import base64 + +import json +from psycopg2 import IntegrityError +from openerp import http, SUPERUSER_ID +from openerp.http import request +from openerp.tools.translate import _ +from openerp.exceptions import ValidationError +from openerp.addons.base.ir.ir_qweb import nl2br + +class WebsiteForm(http.Controller): + + # Check and insert values from the form on the model + @http.route('/website_form/', type='http', auth="public", methods=['POST'], website=True) + def website_form(self, model_name, **kwargs): + model_record = request.env['ir.model'].search([('model', '=', model_name), ('website_form_access', '=', True)]) + + if not model_record: + return json.dumps(False) + + try: + data = self.extract_data(model_record, ** kwargs) + + # If we encounter an issue while extracting data + except ValidationError, e: + # I couldn't find a cleaner way to pass data to an exception + return json.dumps({'error_fields' : e.args[0]}) + + try: + id_record = self.insert_record(request, model_record, data['record'], data['custom'], data.get('meta')) + if id_record: + self.insert_attachment(model_record, id_record, data['attachments']) + + # Some fields have additionnal SQL constraints that we can't check generically + # Ex: crm.lead.probability which is a float between 0 and 1 + # TODO: How to get the name of the erroneous field ? + except IntegrityError: + return json.dumps(False) + + request.session['form_builder_model'] = model_record.name + request.session['form_builder_id'] = id_record + + return json.dumps({'id': id_record}) + + # Constants string to make custom info and metadata readable on a text field + + _custom_label = "%s\n___________\n\n" % _("Custom infos") # Title for custom fields + _meta_label = "%s\n________\n\n" % _("Metadata") # Title for meta data + + # Dict of dynamically called filters following type of field to be fault tolerent + + def identity(self, field_label, field_input): + return field_input + + def integer(self, field_label, field_input): + return int(field_input) + + def floating(self, field_label, field_input): + return float(field_input) + + def boolean(self, field_label, field_input): + return bool(field_input) + + def binary(self, field_label, field_input): + return base64.b64encode(field_input.read()) + + def one2many(self, field_label, field_input): + return [int(i) for i in field_input.split(',')] + + def many2many(self, field_label, field_input, *args): + return [(args[0] if args else (6,0)) + (self.one2many(field_label, field_input),)] + + _input_filters = { + 'char': identity, + 'text': identity, + 'html': identity, + 'datetime': identity, + 'many2one': integer, + 'one2many': one2many, + 'many2many':many2many, + 'selection': identity, + 'boolean': boolean, + 'integer': integer, + 'float': floating, + 'binary': binary, + } + + + # Extract all data sent by the form and sort its on several properties + def extract_data(self, model, **kwargs): + + data = { + 'record': {}, # Values to create record + 'attachments': [], # Attached files + 'custom': '', # Custom fields values + } + + authorized_fields = model.get_authorized_fields(); + error_fields = [] + + for field_name, field_value in kwargs.items(): + # If the value of the field if a file + if hasattr(field_value, 'filename'): + # Undo file upload field name indexing + field_name = field_name.rsplit('[', 1)[0] + + # If it's an actual binary field, convert the input file + # If it's not, we'll use attachments instead + if field_name in authorized_fields and authorized_fields[field_name]['type'] == 'binary': + data['record'][field_name] = base64.b64encode(field_value.read()) + else: + field_value.field_name = field_name + data['attachments'].append(field_value) + + # If it's a known field + elif field_name in authorized_fields: + try: + input_filter = self._input_filters[authorized_fields[field_name]['type']] + data['record'][field_name] = input_filter(self, field_name, field_value) + except ValueError: + error_fields.append(field_name) + + # If it's a custom field + elif field_name != 'context': + data['custom'] += "%s : %s\n" % (field_name, field_value) + + # Add metadata if enabled + environ = request.httprequest.headers.environ + if(request.website.website_form_enable_metadata): + data['meta'] += "%s : %s\n%s : %s\n%s : %s\n%s : %s\n" % ( + "IP" , environ.get("REMOTE_ADDR"), + "USER_AGENT" , environ.get("HTTP_USER_AGENT"), + "ACCEPT_LANGUAGE" , environ.get("HTTP_ACCEPT_LANGUAGE"), + "REFERER" , environ.get("HTTP_REFERER") + ) + + # This function can be defined on any model to provide + # a model-specific filtering of the record values + # Example: + # def website_form_input_filter(self, values): + # values['name'] = '%s\'s Application' % values['partner_name'] + # return values + dest_model = request.env[model.model] + if hasattr(dest_model, "website_form_input_filter"): + data['record'] = dest_model.website_form_input_filter(request, data['record']) + + missing_required_fields = [label for label, field in authorized_fields.iteritems() if field['required'] and not label in data['record']] + if any(error_fields): + raise ValidationError(error_fields + missing_required_fields) + + return data + + def insert_record(self, request, model, values, custom, meta=None): + record = request.env[model.model].sudo().create(values) + + if custom or meta: + default_field_name = model.website_form_default_field_id.name + default_field_data = values.get(default_field_name, '') + custom_content = (default_field_data + "\n\n" if default_field_data else '') \ + + (self._custom_label + custom + "\n\n" if custom else '') \ + + (self._meta_label + meta if meta else '') + + # If there is a default field configured for this model, use it. + # If there isn't, put the custom data in a message instead + if default_field_name: + record.update({default_field_name: custom_content}) + else: + values = { + 'body': nl2br(custom_content), + 'model': model.model, + 'message_type': 'comment', + 'no_auto_thread': False, + 'res_id': record.id, + } + mail_id = request.env['mail.message'].sudo().create(values) + + return record.id + + # Link all files attached on the form + def insert_attachment(self, model, id_record, files): + orphan_attachment_ids = [] + record = model.env[model.model].browse(id_record) + authorized_fields = model.get_authorized_fields() + for file in files: + custom_field = file.field_name not in authorized_fields + attachment_value = { + 'name': file.field_name if custom_field else file.filename, + 'datas': base64.encodestring(file.read()), + 'datas_fname': file.filename, + 'res_model': model.model, + 'res_id': record.id, + } + attachment_id = request.env['ir.attachment'].sudo().create(attachment_value) + if attachment_id and not custom_field: + record.sudo()[file.field_name] = [(4, attachment_id.id)] + else: + orphan_attachment_ids.append(attachment_id.id) + + # If some attachments didn't match a field on the model, + # we create a mail.message to link them to the record + if orphan_attachment_ids: + if model.name != 'mail.mail': + values = { + 'body': _('

Attached files :

'), + 'model': model.model, + 'message_type': 'comment', + 'no_auto_thread': False, + 'res_id': id_record, + 'attachment_ids': [(6, 0, orphan_attachment_ids)], + } + mail_id = request.env['mail.message'].sudo().create(values) + else: + # If the model is mail.mail then we have no other choice but to + # attach the custom binary field files on the attachment_ids field. + for attachment_id_id in orphan_attachment_ids: + record.attachment_ids = [(4, attachment_id_id)] diff --git a/addons/website_form/models/__init__.py b/addons/website_form/models/__init__.py new file mode 100644 index 00000000000..bff786c0885 --- /dev/null +++ b/addons/website_form/models/__init__.py @@ -0,0 +1 @@ +import models diff --git a/addons/website_form/models/models.py b/addons/website_form/models/models.py new file mode 100644 index 00000000000..8fdc5e4bf9a --- /dev/null +++ b/addons/website_form/models/models.py @@ -0,0 +1,47 @@ +from openerp import tools +from openerp import models, fields, api + + +class website_form_config(models.Model): + _inherit = 'website' + website_form_enable_metadata = fields.Boolean('Write metadata',help="Enable writing metadata on form submit.") + + +class website_form_model(models.Model): + _name = 'ir.model' + _inherit = 'ir.model' + + website_form_access = fields.Boolean('Allowed to use in forms', help='Enable the form builder feature for this model.') + website_form_default_field_id = fields.Many2one('ir.model.fields', 'Field for custom form data', domain="[('model', '=', model), ('ttype', '=', 'text')]", help="Specify the field wich will contain meta and custom form fields datas.") + website_form_label = fields.Char("Label for form action", help="Form action label. Ex: crm.lead could be 'Send an e-mail' and project.issue could be 'Create an Issue'.") + + + def all_inherited_model_ids(self): + return [self.id] + [m.all_inherited_model_ids() for m in self.inherited_model_ids] + + @api.multi + def get_authorized_fields(self): + model = self.env[self.model] + fields_get = model.fields_get() + domain = [('model_id', 'in', self.all_inherited_model_ids()), ('website_form_blacklisted', '=', False)] + + for elem in self.env['ir.model.fields'].search(domain): + if elem.website_form_blacklisted: + fields_get.pop(elem.name, None) + for key, val in model._inherits.iteritems(): + fields_get.pop(val,None) + + # Unrequire fields with default values + default_values = model.default_get(fields_get.keys()) + for field in [f for f in fields_get if f in default_values]: + fields_get[field]['required'] = False + + return fields_get + + +class website_form_model_fields(models.Model): + """ fields configuration for form builder """ + _name = 'ir.model.fields' + _inherit = 'ir.model.fields' + + website_form_blacklisted = fields.Boolean('Blacklisted in web forms', help='Blacklist this field for web forms') diff --git a/addons/website_form/static/src/js/website_form.js b/addons/website_form/static/src/js/website_form.js new file mode 100644 index 00000000000..88859643129 --- /dev/null +++ b/addons/website_form/static/src/js/website_form.js @@ -0,0 +1,168 @@ +odoo.define('website_form.animation', function (require) { +'use strict'; + + var core = require('web.core'); + var time = require('web.time'); + var ajax = require('web.ajax'); + var snippet_animation = require('web_editor.snippets.animation'); + + var _t = core._t; + var qweb = core.qweb; + + + snippet_animation.registry.form_builder_send = snippet_animation.Class.extend({ + selector: '.s_website_form', + + start: function() { + var self = this; + qweb.add_template('/website_form/static/src/xml/website_form.xml'); + this.$target.find('.o_website_form_send').on('click',function(e) {self.send(e);}); + + // Initialize datetimepickers + var l10n = _t.database.parameters; + this.$target.find('.o_website_form_datetime').datetimepicker({ + pickTime: true, + useSeconds: true, + startDate: moment({ y: 1900 }), + endDate: moment().add(200, "y"), + calendarWeeks: true, + icons : { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + up: 'fa fa-chevron-up', + down: 'fa fa-chevron-down' + }, + language : moment.locale(), + format : time.strftime_to_moment_format(l10n.date_format +' '+ l10n.time_format), + }); + }, + + stop: function() { + this.$target.find('button').off('click'); + }, + + send: function(e) { + e.preventDefault(); // Prevent the default submit behavior + + var self = this; + + self.$target.find('#o_website_form_result').empty(); + if (!self.check_error_fields([])) { + self.update_status('invalid'); + return false; + } + + // Prepare form inputs + this.form_fields = this.$target.serializeArray(); + _.each(this.$target.find('input[type=file]'), function(input) { + $.each($(input).prop('files'), function(index, file) { + // Index field name as ajax won't accept arrays of files + // when aggregating multiple files into a single field value + self.form_fields.push({ + name: input.name + '[' + index + ']', + value: file + }); + }); + }); + + // Serialize form inputs into a single object + // Aggregate multiple values into arrays + var form_values = {}; + _.each(this.form_fields, function(input) { + if (input.name in form_values) { + // If a value already exists for this field, + // we are facing a x2many field, so we store + // the values in an array. + if (Array.isArray(form_values[input.name])) { + form_values[input.name].push(input.value); + } else { + form_values[input.name] = [form_values[input.name], input.value]; + } + } else { + if (input.value != '') { + form_values[input.name] = input.value; + } + } + }); + + // Overwrite form_values array with values from the form tag + // Necessary to handle field values generated server-side, since + // using t-att- inside a snippet makes it non-editable ! + for (var key in this.$target.data()) { + if (_.str.startsWith(key, 'form_field_')){ + form_values[key.replace('form_field_', '')] = this.$target.data(key); + } + } + + // Post form and handle result + ajax.post(this.$target.attr('action') + this.$target.data('model_name'), form_values) + .then(function(result_data) { + result_data = $.parseJSON(result_data); + if(!result_data.id) { + // Failure, the server didn't return the created record ID + self.update_status('error'); + if (result_data.error_fields && result_data.error_fields.length) { + // If the server return a list of bad fields, show these fields for users + self.check_error_fields(result_data.error_fields); + } + } else { + // Success, redirect or update status + var success_page = self.$target.attr('data-success_page'); + if(success_page) { + $(location).attr('href', success_page); + } + else { + self.update_status('success'); + } + } + }) + .fail(function(result_data){ + self.update_status('error'); + }); + }, + + check_error_fields: function(error_fields) { + var form_valid = true; + // Loop on all fields + this.$target.find('.form-field').each(function(k, field){ + var $field = $(field); + var $fields = self.$fields; + var field_name = $field.find('.control-label').attr('for') + + // Validate inputs for this field + var field_valid = true; + var inputs = $field.find('.o_website_form_input'); + var invalid_inputs = inputs.toArray().filter(function(input, k, inputs) { + // Special check for multiple required checkbox for same + // field as it seems checkValidity forces every required + // checkbox to be checked, instead of looking at other + // checkboxes with the same name and only requiring one + // of them to be checked. + if (input.required && input.type == 'checkbox') { + // Considering we are currently processing a single + // field, we can assume that all checkboxes in the + // inputs variable have the same name + var checkboxes = _.filter(inputs, function(input){ + return input.required && input.type == 'checkbox' + }) + return !_.any(checkboxes, function(checkbox){return checkbox.checked}) + } else { + return !input.checkValidity(); + } + }) + + // Update field color if invalid or erroneous + $field.removeClass('has-error'); + if(invalid_inputs.length || error_fields.indexOf(field_name) >= 0){ + $field.addClass('has-error'); + form_valid = false; + } + }); + return form_valid; + }, + + update_status: function(status) { + this.$target.find('#o_website_form_result').replaceWith(qweb.render("website_form.status_" + status)) + }, + }); +}); diff --git a/addons/website_form/static/src/less/website_form.less b/addons/website_form/static/src/less/website_form.less new file mode 100644 index 00000000000..f26718da9c1 --- /dev/null +++ b/addons/website_form/static/src/less/website_form.less @@ -0,0 +1,54 @@ +// Radio buttons and checkboxes flex layout +.o_website_form_flex { + display: flex; + flex-wrap: wrap; +} + +.o_website_form_flex_item { + // col-md-4 + flex-basis: 33%; + + // col-sm-6 + @media (max-width: @screen-sm-max) { + flex-basis: 50%; + } + + // col-xs-12 + @media (max-width: @screen-xs-max) { + flex-basis: 100%; + } +} + +// Hidden field is only partially hidden in editor +.o_website_form_field_hidden { + display: none; +} + +.editor_enable .o_website_form_field_hidden { + display: block; + opacity: 0.5; +} + +// Select inputs do not trigger the default browser behavior +// Since we use a custom editable element +.editor_enable .s_website_form .form-field select { + pointer-events: none; +} + +// Required fields have a star which is not part of the field label +.o_website_form_required, .o_website_form_required_custom { + .control-label:after { + content: ' *'; + } +} + +// Quickfix to display the editable select as a single big field +#editable_select.form-control { + height: 100%; +} + +// Fix for firefox browse button which is too big for Bootstrap form-field +// http://stackoverflow.com/questions/22049739/fix-for-firefox-file-input-using-bootstrap-3-1 +.form-field input[type=file].form-control { + height: 100%; +} diff --git a/addons/website_form/static/src/xml/website_form.xml b/addons/website_form/static/src/xml/website_form.xml new file mode 100644 index 00000000000..3a1102bb5e1 --- /dev/null +++ b/addons/website_form/static/src/xml/website_form.xml @@ -0,0 +1,25 @@ + + + + + + + The form has been sent successfully. + + + + + + + Please fill in the form correctly. + + + + + + + An error has occured, the form has not been sent. + + + + diff --git a/addons/website_form/views/assets.xml b/addons/website_form/views/assets.xml new file mode 100644 index 00000000000..0cf95cca454 --- /dev/null +++ b/addons/website_form/views/assets.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/addons/website_issue/__init__.py b/addons/website_issue/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/website_issue/__openerp__.py b/addons/website_issue/__openerp__.py new file mode 100644 index 00000000000..b6efff77bc8 --- /dev/null +++ b/addons/website_issue/__openerp__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Issues Form', + 'category': 'Bridge', + 'summary': 'Create Issues From Contact Form', + 'version': '1.0', + 'description': """ +OpenERP Contact Form +==================== + + """, + 'author': 'OpenERP SA', + 'depends': ['website_form','project_issue'], + 'data': [ + 'data/website_issue_data.xml', + ], + 'installable': True, + 'auto_install': True, +} diff --git a/addons/website_issue/data/website_issue_data.xml b/addons/website_issue/data/website_issue_data.xml new file mode 100644 index 00000000000..a588449d30d --- /dev/null +++ b/addons/website_issue/data/website_issue_data.xml @@ -0,0 +1,15 @@ + + + + + + True + Create an issue + + + +