diff options
| author | David Monjoie <dmo@odoo.com> | 2015-08-25 21:50:31 +0200 |
|---|---|---|
| committer | David Monjoie <dmo@odoo.com> | 2015-08-26 16:01:19 +0200 |
| commit | a77f5cf42faa75a2dd3931d83ad8ea86648248c0 (patch) | |
| tree | 29590b9351bf73ac573928be7f0dbaa5ea8505a8 | |
| parent | 57c4787d09eacc548140d7a552a76264eae21a55 (diff) | |
[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.
34 files changed, 1047 insertions, 305 deletions
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 @@ <?xml version="1.0" encoding="utf-8"?> <openerp> <data noupdate="1"> + <record id="crm.model_crm_lead" model="ir.model"> + <field name="website_form_default_field_id" ref="crm.field_crm_lead_description" /> + <field name="website_form_access">True</field> + <field name="website_form_label">Create a lead</field> + </record> <record id="action_open_website" model="ir.actions.act_url"> <field name="name">Website Contact Form</field> <field name="target">self</field> <field name="url">/page/contactus</field> </record> + <record id="base.open_menu" model="ir.actions.todo"> <field name="action_id" ref="action_open_website"/> <field name="state">open</field> @@ -19,6 +25,5 @@ <field name="use_leads">True</field> <field name="use_opportunities" eval="False"/> </record> - </data> </openerp> 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('<div id="website_form_success_test_tour_'+model+'"></div>'); + } + }; + 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 @@ <?xml version="1.0" encoding="utf-8"?> <openerp> -<data> - -<template id="contactus_form" name="Contact Form" inherit_id="website.contactus" customize_show="True"> - <xpath expr="//div[@name='mail_button']" position="replace"> - <form action="/crm/contactus" method="post" class="form-horizontal mt32 js_website_submit_form" enctype="multipart/form-data"> - <div t-attf-class="form-group #{error and 'contact_name' in error and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="contact_name">Your Name</label> - <div class="col-md-7 col-sm-8"> - <input type="text" class="form-control" name="contact_name" required="True" t-attf-value="#{contact_name or ''}"/> - </div> - </div> - <div t-attf-class="form-group #{error and 'phone' in error and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="phone">Phone Number</label> - <div class="col-md-7 col-sm-8"> - <input type="text" class="form-control" name="phone" t-attf-value="#{phone or ''}"/> - </div> - </div> - <div name="email_from_container" t-attf-class="form-group #{error and 'email_from' in error and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="email_from">Email</label> - <div class="col-md-7 col-sm-8"> - <input type="email" class="form-control" name="email_from" required="True" t-attf-value="#{email_from or ''}"/> - </div> - </div> - <div t-attf-class="form-group #{error and 'name' in error and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="name">Subject</label> - <div class="col-md-7 col-sm-8"> - <input type="text" class="form-control" name="name" t-attf-value="#{name or ''}"/> - </div> - </div> - <div t-attf-class="form-group #{error and 'description' in error and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="description">Your Question</label> - <div class="col-md-7 col-sm-8"> - <textarea class="form-control" name="description" style="min-height: 120px" required="True"><t t-esc="description or ''"/></textarea> - </div> - </div> - <t t-foreach="kwargs" t-as="kwarg"> - <input type="hidden" t-att-name="kwarg[0]" t-att-value="kwarg[1]"/> - </t> - - <div class="form-group"> - <div class="col-md-offset-3 col-sm-offset-4 col-sm-8 col-md-7"> - <button type="submit" class="btn btn-primary btn-lg">Send</button> + <data> + <template id="contactus_form" name="Contact Form" inherit_id="website.contactus" customize_show="True"> + <xpath expr="//div[@name='mail_button']" position="replace"> + <div> + <form action="/website_form/" method="post" data-model_name="crm.lead" data-success_page="/page/website_crm.contactus_thanks" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data"> + <div class="form-group form-field o_website_form_required_custom"> + <label class="col-md-3 col-sm-4 control-label" for="contact_name">Your Name</label> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="contact_name" required=""/> + </div> + </div> + <div class="form-group form-field"> + <label class="col-md-3 col-sm-4 control-label" for="phone">Phone Number</label> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" placeholder="e.g. (+32).81.81.37.00" name="phone"/> + </div> + </div> + <div class="form-group form-field o_website_form_required_custom"> + <label class="col-md-3 col-sm-4 control-label" for="email_from">Email</label> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="email_from" required=""/> + </div> + </div> + <div class="form-group form-field o_website_form_required_custom"> + <label class="col-md-3 col-sm-4 control-label" for="partner_name">Your Company</label> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="partner_name" required=""/> + </div> + </div> + <div class="form-group form-field o_website_form_required"> + <label class="col-md-3 col-sm-4 control-label" for="name">Subject</label> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="name" required=""/> + </div> + </div> + <div class="form-group form-field o_website_form_required_custom"> + <label class="col-md-3 col-sm-4 control-label" for="description">Your Question</label> + <div class="col-md-7 col-sm-8"> + <textarea class="form-control o_website_form_input" name="description" required=""></textarea> + </div> + </div> + <div class="form-group"> + <div class="col-md-offset-3 col-sm-offset-4 col-sm-8 col-md-7"> + <button class="btn btn-primary btn-lg o_website_form_send">Send</button> + <span id="o_website_form_result"></span> + </div> + </div> + </form> </div> - </div> - </form> - </xpath> -</template> - -<template id="contactus_form_company_name" name="Company Name" inherit_id="website_crm.contactus_form" customize_show="True"> - <xpath expr="//div[@name='email_from_container']" position="after"> - <div t-attf-class="form-group #{error and 'partner_name' in error and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="partner_name">My Company</label> - <div class="col-md-7 col-sm-8"> - <input type="text" class="form-control" name="partner_name" required="True" t-attf-value="#{partner_name or ''}"/> - </div> - </div> - </xpath> -</template> + </xpath> + </template> -<template id="contactus_thanks" name="Contact us"> - <t t-call="website.layout"> - <div id="wrap"> - <div class="oe_structure"/> - <div class="container"> - <h1>Thanks!</h1> - <div class="row"> - <div class="col-md-8"> - <div class="alert alert-success"> - Your message has been sent successfully. - <button type="button" class="close" data-dismiss="alert">&times;</button> + <template id="contactus_thanks" name="Contact us"> + <t t-call="website.layout"> + <div id="wrap"> + <div class="oe_structure"/> + <div class="container"> + <h1>Thanks!</h1> + <div class="row"> + <div class="col-md-8"> + <div class="alert alert-success"> + Your message has been sent successfully. + <button type="button" class="close" data-dismiss="alert">&times;</button> + </div> + <p> + We will get back to you shortly. + </p><p class="mt64"> + If you have an emergency, do not hesitate to contact us by phone: + </p> + <ul class="list-unstyled"> + <li><i class="fa fa-phone"></i> : <span t-field="res_company.phone"/></li> + <li><i class="fa fa-envelope"></i> : <span t-field="res_company.email"/></li> + </ul> + </div> + <div class="col-md-4"> + <t t-call="website.company_description"/> + </div> </div> - <p> - We will get back to you shortly. - </p><p class="mt64"> - If you have an emergency, do not hesitate to contact us by phone: - </p> - <ul class="list-unstyled"> - <li><i class="fa fa-phone"></i> : <span t-field="res_company.phone"/></li> - <li><i class="fa fa-envelope"></i> : <span t-field="res_company.email"/></li> - </ul> </div> - <div class="col-md-4"> - <t t-call="website.company_description"/> - </div> - </div> - </div> - <div class="oe_structure"/> - </div> - </t> -</template> + <div class="oe_structure"/> + </div> + </t> + </template> + + <template id="website_crm_tests" name="Website CRM Tests" inherit_id="web.assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/website_crm/static/src/js/website_crm_tour.js"></script> + </xpath> + </template> -</data> + </data> </openerp> 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/<string:model_name>', 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 += '<br/><b>%s</b>: <b>%s</b>' % (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('<a href="%s" target="_blank"><b>%s</b></a>' % (url_encoded, url_encoded)) body = '<br/>'.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 index 8ee9bae18d9..8ee9bae18d9 100644 --- a/addons/website_crm/controllers/__init__.py +++ b/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 <model> + @http.route('/website_form/<string:model_name>', 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': _('<p>Attached files : </p>'), + '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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <!-- Success status --> + <t t-name="website_form.status_success"> + <span id="o_website_form_result" class="text-success ml8"> + <i class="fa fa-check mr4"/>The form has been sent successfully. + </span> + </t> + + <!-- Missing field status --> + <t t-name="website_form.status_invalid"> + <span id="o_website_form_result" class="text-danger ml8"> + <i class="fa fa-close mr4"/>Please fill in the form correctly. + </span> + </t> + + <!-- Error status --> + <t t-name="website_form.status_error"> + <span id="o_website_form_result" class="text-danger ml8"> + <i class="fa fa-close mr4"/>An error has occured, the form has not been sent. + </span> + </t> + +</templates> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + + <template id="assets_frontend" name="Website Form Assets Frontend" inherit_id="website.assets_frontend"> + <xpath expr="." position="inside"> + <link rel='stylesheet' href='/website_form/static/src/less/website_form.less'/> + <script type="text/javascript" src="/website_form/static/src/js/website_form.js"/> + + <!-- Bootstrap datetimepicker is currently only included in the backend --> + <link rel="stylesheet" href="/web/static/lib/bootstrap-datetimepicker/css/bootstrap-datetimepicker.css"/> + <script type="text/javascript" src="/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js"/> + </xpath> + </template> + + </data> +</openerp> diff --git a/addons/website_hr_recruitment/__openerp__.py b/addons/website_hr_recruitment/__openerp__.py index 53b6cbca8b6..372e1de127c 100644 --- a/addons/website_hr_recruitment/__openerp__.py +++ b/addons/website_hr_recruitment/__openerp__.py @@ -8,7 +8,7 @@ OpenERP Contact Form ==================== """, - 'depends': ['website_partner', 'hr_recruitment', 'website_mail'], + 'depends': ['website_partner', 'hr_recruitment', 'website_mail', 'website_form'], 'data': [ 'security/ir.model.access.csv', 'security/website_hr_recruitment_security.xml', diff --git a/addons/website_hr_recruitment/controllers/main.py b/addons/website_hr_recruitment/controllers/main.py index 4bef1dfe0d8..5e5dd48e18b 100644 --- a/addons/website_hr_recruitment/controllers/main.py +++ b/addons/website_hr_recruitment/controllers/main.py @@ -89,40 +89,3 @@ class website_hr_recruitment(http.Controller): 'error': error, 'default': default, }) - - @http.route('/jobs/thankyou', methods=['POST'], type='http', auth="public", website=True) - def jobs_thankyou(self, **post): - error = {} - for field_name in ["partner_name", "phone", "email_from"]: - if not post.get(field_name): - error[field_name] = 'missing' - if error: - request.session['website_hr_recruitment_error'] = error - ufile = post.pop('ufile') - if ufile: - error['ufile'] = 'reset' - request.session['website_hr_recruitment_default'] = post - return request.redirect('/jobs/apply/%s' % post.get("job_id")) - - # public user can't create applicants (duh) - env = request.env(user=SUPERUSER_ID) - value = { - 'name': '%s\'s Application' % post.get('partner_name'), - } - for f in ['email_from', 'partner_name', 'description']: - value[f] = post.get(f) - for f in ['department_id', 'job_id']: - value[f] = int(post.get(f) or 0) - # Retro-compatibility for saas-3. "phone" field should be replace by "partner_phone" in the template in trunk. - value['partner_phone'] = post.pop('phone', False) - - applicant = env['hr.applicant'].create(value) - if post['ufile']: - name = applicant.partner_name if applicant.partner_name else applicant.name - applicant.message_post( - body = _("%s's Application \n From: %s \n\n %s \n") % (name, applicant.email_from or "", applicant.description or ""), - attachments = [(post['ufile'].filename, post['ufile'].read())], - content_subtype = 'plaintext', - subtype = "hr_recruitment.mt_applicant_hired") - - return request.render("website_hr_recruitment.thankyou", {}) diff --git a/addons/website_hr_recruitment/data/config_data.xml b/addons/website_hr_recruitment/data/config_data.xml index fe6192d351f..ee934bb3ca4 100644 --- a/addons/website_hr_recruitment/data/config_data.xml +++ b/addons/website_hr_recruitment/data/config_data.xml @@ -16,6 +16,10 @@ <field name="action_id" ref="action_open_website"/> <field name="state">open</field> </record> - + <record id="hr_recruitment.model_hr_applicant" model="ir.model"> + <field name="website_form_default_field_id" ref="hr_recruitment.field_hr_applicant_description" /> + <field name="website_form_access">True</field> + <field name="website_form_label">Apply for a Job</field> + </record> </data> </openerp> diff --git a/addons/website_hr_recruitment/models/__init__.py b/addons/website_hr_recruitment/models/__init__.py index b9c76c98f6e..331192a2437 100644 --- a/addons/website_hr_recruitment/models/__init__.py +++ b/addons/website_hr_recruitment/models/__init__.py @@ -1,2 +1,3 @@ import hr_job import hr_recruitment_source +import hr_applicant diff --git a/addons/website_hr_recruitment/models/hr_applicant.py b/addons/website_hr_recruitment/models/hr_applicant.py new file mode 100644 index 00000000000..de1070ad43f --- /dev/null +++ b/addons/website_hr_recruitment/models/hr_applicant.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv + +class hr_applicant(osv.Model): + _inherit = 'hr.applicant' + + def website_form_input_filter(self, request, values): + if 'partner_name' in values: + values.setdefault('name', '%s\'s Application' % values['partner_name']) + return values diff --git a/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_tour.js b/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_tour.js new file mode 100644 index 00000000000..94297e88746 --- /dev/null +++ b/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_tour.js @@ -0,0 +1,92 @@ +odoo.define('website_hr_recruitment.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_hr_recruitment_tour', + name: "Test the hr applicants form", + path: '/jobs/apply/3', + mode: 'test', + steps: [ + { + title: "Complete name", + element: "input[name=partner_name]", + sampleText: "John Smith" + }, + { + title: "Complete Email", + element: "input[name=email_from]", + sampleText: "john@smith.com" + }, + { + title: "Complete phone number", + element: "input[name=partner_phone]", + sampleText: "118.218" + }, + { + title: "Complete Subject", + element: "textarea[name=description]", + sampleText: "A completely useless message" + }, + // TODO: Upload a file ? + { + title: "Send the form", + element: ".o_website_form_send" + }, + { + title: "Check the form is submited without errors", + waitFor: ".oe_structure:has(h1:contains('Thank you!'))" + } + ] + }); + + Tour.register({ + id: 'website_hr_recruitment_tour_results', + name: "Check the hr applicants form created records", + path: '', + mode: 'test', + steps: [ + { + title: "Check hr.applicant record has been created", + waitFor: ".o-apps", + onload: function (tour) { + var hrDef = new Model("hr.applicant").call( + "search_read", + [ + [ + ['partner_name' , '=' , 'John Smith'], + ['partner_phone' , '=' , '118.218'], + ['email_from' , '=' , 'john@smith.com'], + ['description' , 'like', 'A completely useless message'] + ], + [] + ] + ); + var success = function(model, data) { + if(data.length) { + $('body').append('<div id="website_form_success_test_tour_'+model+'"></div>'); + } + }; + hrDef.then(_.bind(success, this, 'hr_applicant')); + } + }, + { + title: "Check hr.applicant record has been created", + waitFor: "#website_form_success_test_tour_hr_applicant" + }, + { + title: "Final Step", + waitFor: "html" + } + ] + }); + }); + + return {}; +}); diff --git a/addons/website_hr_recruitment/tests/__init__.py b/addons/website_hr_recruitment/tests/__init__.py new file mode 100644 index 00000000000..3b663956ae8 --- /dev/null +++ b/addons/website_hr_recruitment/tests/__init__.py @@ -0,0 +1 @@ +import test_website_hr_recruitment
\ No newline at end of file diff --git a/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py b/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py new file mode 100644 index 00000000000..d3cb0150bd9 --- /dev/null +++ b/addons/website_hr_recruitment/tests/test_website_hr_recruitment.py @@ -0,0 +1,7 @@ +import openerp.tests +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) +class TestWebsiteHrRecruitmentForm(openerp.tests.HttpCase): + def test_tour(self): + self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('website_hr_recruitment_tour', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.website_hr_recruitment_tour", login="admin") + self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('website_hr_recruitment_tour_results', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.website_hr_recruitment_tour_results", login="admin") diff --git a/addons/website_hr_recruitment/views/templates.xml b/addons/website_hr_recruitment/views/templates.xml index f8df63cef5c..4550da6db4e 100644 --- a/addons/website_hr_recruitment/views/templates.xml +++ b/addons/website_hr_recruitment/views/templates.xml @@ -140,60 +140,66 @@ <template id="apply"> <t t-call="website.layout"> <t t-set="additional_title">Apply Job</t> - <div id="wrap"> - <div class="container"> - <h1 class="text-center"> - Job Application Form - </h1> - <h2 t-if="job" class="text-center text-muted"> - <span t-field="job.name"/> - </h2> + + <div id="wrap" class="container"> + <h1 class="text-center"> + Job Application Form + </h1> + <h2 t-if="job" class="text-center text-muted"> + <span t-field="job.name"/> + </h2> - <div class="row"> - <section id="forms"> - <!-- TODO Multilingual form action support ? --> - <form class="form-horizontal mt32 js_website_submit_form" action="/jobs/thankyou" method="post" enctype="multipart/form-data"> - <input type="hidden" t-att-value="job and job.department_id.id or False" name="department_id"/> - <input type="hidden" t-att-value="job and job.id or False" name="job_id"/> - <div t-attf-class="form-group #{error.get('partner_name') and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="partner_name">Your Name</label> - <div class="col-md-7 col-sm-8"> - <input type="text" t-att-value="default.get('partner_name')" class="form-control" name="partner_name" required="True"/> - </div> + <div class="row"> + <section id="forms"> + <form action="/website_form/" method="post" class="s_website_form form-horizontal container-fluid mt32" enctype="multipart/form-data" data-model_name="hr.applicant" data-success_page="/page/website_hr_recruitment.thankyou" t-att-data-form_field_department_id="job and job.department_id.id or False" t-att-data-form_field_job_id="job and job.id or False"> + <div class="form-group form-field o_website_form_required_custom"> + <div class="col-md-3 col-sm-4 text-right"> + <label class="control-label" for="partner_name">Your Name</label> </div> - <div t-attf-class="form-group #{error.get('email_from') and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="email_from">Your Email</label> - <div class="col-md-7 col-sm-8"> - <input type="email" t-att-value="default.get('email_from')" class="form-control" name="email_from" required="True"/> - </div> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="partner_name" required=""/> </div> - <div t-attf-class="form-group #{error.get('phone') and 'has-error' or ''}"> - <label class="col-md-3 col-sm-4 control-label" for="phone">Your Phone</label> - <div class="col-md-7 col-sm-8"> - <input type="text" t-att-value="default.get('phone')" class="form-control" name="phone" required="True"/> - </div> + </div> + <div class="form-group form-field o_website_form_required_custom"> + <div class="col-md-3 col-sm-4 text-right"> + <label class="control-label" for="email_from">Your Email</label> </div> - <div class="form-group"> - <label class="col-md-3 col-sm-4 control-label" for="description">Short Introduction</label> - <div class="col-md-7 col-sm-8"> - <textarea class="form-control" t-esc="default.get('description')" name="description" style="min-height: 120px"/> - </div> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="email_from" required=""/> </div> - <div class="form-group"> - <label class="col-md-3 col-sm-4 control-label" for="ufile">Resume</label> - <div class="col-md-7 col-sm-8"> - <input class="input-file" id="fileInput" type="file" name="ufile"/> - <span t-if="error.get('ufile')">Please send again your resume.</span> - </div> + </div> + <div class="form-group form-field o_website_form_required_custom"> + <div class="col-md-3 col-sm-4 text-right"> + <label class="control-label" for="partner_phone">Your Phone Number</label> </div> - <div class="form-group"> - <div class="col-md-offset-3 col-sm-offset-4 col-sm-8 col-md-7"> - <button type="submit" class="btn btn-primary">Submit</button> - </div> + <div class="col-md-7 col-sm-8"> + <input type="text" class="form-control o_website_form_input" name="partner_phone" required=""/> </div> - </form> - </section> - </div> + </div> + <div class="form-group form-field"> + <div class="col-md-3 col-sm-4 text-right"> + <label class="control-label" for="description">Short Introduction</label> + </div> + <div class="col-md-7 col-sm-8"> + <textarea class="form-control o_website_form_input" name="description"></textarea> + </div> + </div> + <div class="form-group form-field o_website_form_custom"> + <div class="col-md-3 col-sm-4 text-right"> + <label class="control-label" for="Resume">Resume</label> + </div> + <div class="col-md-7 col-sm-8"> + <input type="file" class="form-control o_website_form_input" name="Resume"/> + </div> + </div> + <div class="form-group"> + <div class="col-md-offset-3 col-sm-offset-4 col-sm-8 col-md-7"> + <button class="btn btn-primary btn-lg o_website_form_send">Submit</button> + <span id="o_website_form_result"></span> + </div> + </div> + </form> + </section> </div> </div> </t> @@ -292,5 +298,11 @@ </xpath> </template> +<template id="website_hr_recuitment_tests" name="Website HR Recruitment Tests" inherit_id="web.assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/website_hr_recruitment/static/src/js/website_hr_recruitment_tour.js"></script> + </xpath> +</template> + </data> </openerp> diff --git a/addons/website_issue/__init__.py b/addons/website_issue/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/addons/website_issue/__init__.py 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data noupdate="1"> + <record id="project_issue.model_project_issue" model="ir.model"> + <field name="website_form_default_field_id" ref="project_issue.field_project_issue_description" /> + <field name="website_form_access">True</field> + <field name="website_form_label">Create an issue</field> + </record> +<!-- Template to blacklist field + <record id="project_issue.field_project_issue_{field_name}" model="ir.model.fields"> + <field name="website_form_blacklisted">True</field> + </record> +--> + </data> +</openerp> |
