[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.
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
import controllers
|
||||
import models
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import crm_lead
|
||||
@@ -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
|
||||
@@ -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 {};
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import test_website_crm
|
||||
@@ -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")
|
||||
@@ -1,95 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<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>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
</data>
|
||||
<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>
|
||||
</openerp>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import controllers
|
||||
import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)]
|
||||
@@ -0,0 +1 @@
|
||||
import models
|
||||
@@ -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')
|
||||
@@ -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))
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import hr_job
|
||||
import hr_recruitment_source
|
||||
import hr_applicant
|
||||
|
||||
@@ -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
|
||||
@@ -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 {};
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import test_website_hr_recruitment
|
||||
@@ -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")
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user