[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:
David Monjoie
2015-08-26 16:01:19 +02:00
parent 57c4787d09
commit a77f5cf42f
34 changed files with 1048 additions and 306 deletions
@@ -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
View File
@@ -1 +1 @@
import controllers
import models
+3 -3
View File
@@ -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,
}
-109
View File
@@ -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)
+6 -1
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
import crm_lead
+8
View File
@@ -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 {};
});
+1
View File
@@ -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")
+87 -88
View File
@@ -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">&amp;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">&amp;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>
+25 -18
View File
@@ -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:
+2
View File
@@ -0,0 +1,2 @@
import controllers
import models
+18
View File
@@ -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,
}
+217
View File
@@ -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)]
+1
View File
@@ -0,0 +1 @@
import models
+47
View File
@@ -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>
+17
View File
@@ -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>
+1 -1
View File
@@ -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>
View File
+18
View File
@@ -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>