<group expand="0" string="Group By">
<filter name="group_by_partner_id" string="Partner" context="{'group_by':'partner_id'}"/>
<filter string="Salesperson" context="{'group_by':'user_id'}"/>
- <filter string="Category of Product" name="category_product" context="{'group_by':'categ_id','residual_invisible':True}"/>
<filter string="Status" context="{'group_by':'state'}"/>
<separator/>
<filter string="Period" context="{'group_by':'period_id'}"/>
</p>
</field>
</record>
- <menuitem action="action_invoice_tree2" id="menu_action_invoice_tree2" parent="menu_finance_payables"/>
+ <menuitem action="action_invoice_tree2" id="menu_action_invoice_tree2" parent="menu_finance_payables" sequence="1"/>
<record id="action_invoice_tree3" model="ir.actions.act_window">
<field name="name">Customer Refunds</field>
</p>
</field>
</record>
- <menuitem action="action_invoice_tree4" id="menu_action_invoice_tree4" parent="menu_finance_payables"/>
+ <menuitem action="action_invoice_tree4" id="menu_action_invoice_tree4" parent="menu_finance_payables" sequence="2"/>
<act_window
id="act_account_journal_2_account_invoice_opened"
'stop_datetime': fields.datetime('End Datetime', states={'done': [('readonly', True)]}, track_visibility='onchange'), # old date_deadline
'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
'description': fields.text('Description', states={'done': [('readonly', True)]}),
- 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
+ 'class': fields.selection([('public', 'Everyone'), ('private', 'Only me'), ('confidential', 'Only internal users')], 'Privacy', states={'done': [('readonly', True)]}),
'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
res = False
new_id = False
- # Special write of complex IDS
- for event_id in ids:
+ # Special write of complex IDS
+ for event_id in list(ids):
if len(str(event_id).split('-')) == 1:
continue
if data.get('rrule'):
new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
- res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
+ res = super(calendar_event, self).write(cr, uid, [int(event_id) for event_id in ids], values, context=context)
# set end_date for calendar searching
if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
this.$('.oe_reply').on('click', this.on_message_reply);
this.$('.oe_star').on('click', this.on_star);
this.$('.oe_msg_vote').on('click', this.on_vote);
+ this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover);
this.$('.oe_mail_expand').on('click', this.on_expand);
this.$('.oe_mail_reduce').on('click', this.on_expand);
this.$('.oe_mail_action_model').on('click', this.on_record_clicked);
this.$('.oe_mail_action_author').on('click', this.on_record_author_clicked);
},
-
+ on_hover : function(event){
+ var self = this;
+ var voter = "";
+ var limit = 10;
+ event.stopPropagation();
+ var $target = $(event.target).hasClass("fa-thumbs-o-up") ? $(event.target).parent() : $(event.target);
+ //Note: We can set data-content attr on target element once we fetch data so that next time when one moves mouse on element it saves call
+ //But if there is new like comes then we'll not have new likes in popover in that case
+ if ($target.data('liker-list'))
+ {
+ voter = $target.data('liker-list');
+ self.bindTooltipTo($target, voter);
+ $target.tooltip('hide').tooltip('show');
+ $(".tooltip").on("mouseleave", function () {
+ $(this).remove();
+ });
+ }else{
+ this.ds_message.call('get_likers_list', [this.id, limit])
+ .done(function (data) {
+ _.each(data, function(people, index) {
+ voter = voter + people.substring(0,1).toUpperCase() + people.substring(1);
+ if(index != data.length-1) {
+ voter = voter + "<br/>";
+ }
+ });
+ $target.data('liker-list', voter);
+ self.bindTooltipTo($target, voter);
+ $target.tooltip('hide').tooltip('show');
+ $(".tooltip").on("mouseleave", function () {
+ $(this).remove();
+ });
+ });
+ }
+ return true;
+ },
+ bindTooltipTo: function($el, value) {
+ $el.tooltip({
+ 'title': value,
+ 'placement': 'top',
+ 'container': this.el,
+ 'html': true,
+ 'trigger': 'manual',
+ 'animation': false
+ }).on("mouseleave", function () {
+ setTimeout(function () {
+ if (!$(".tooltip:hover").length) {
+ $el.tooltip("hide");
+ }
+ },100);
+ });
+ },
on_record_clicked: function (event) {
event.preventDefault();
var self = this;
this.$(".oe_msg_footer:first .oe_mail_vote_count").remove();
this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element);
this.$('.oe_msg_vote').on('click', this.on_vote);
+ this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover);
},
/**
start: function () {
this._super.apply(this, arguments);
this.bind_events();
+ return $.when();
},
/* instantiate the compose message object and insert this on the DOM.
message_fetch: function (replace_domain, replace_context, ids, callback) {
return this.ds_message.call('message_read', [
// ids force to read
- ids === false ? undefined : ids,
+ ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit),
// domain + additional
(replace_domain ? replace_domain : this.domain),
// ids allready loaded
// context + additional
(replace_context ? replace_context : this.context),
// parent_id
- this.context.default_parent_id || undefined
+ this.context.default_parent_id || undefined,
+ this.options.fetch_limit,
]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message')
).done(this.proxy('message_fetch_set_read'));
},
'compose_as_todo' : false,
'readonly' : false,
'emails_from_on_composer': true,
+ 'fetch_limit': 30 // limit of chatter messages
}, this.action.params);
this.action.params.help = this.action.help || false;
start: function (options) {
this._super.apply(this, arguments);
this.message_render();
- this.bind_events();
},
/**
},
- bind_events: function () {
- $(document).scroll( _.bind(this.thread.on_scroll, this.thread) );
- $(window).resize( _.bind(this.thread.on_scroll, this.thread) );
- this.$el.resize( _.bind(this.thread.on_scroll, this.thread) );
- window.setTimeout( _.bind(this.thread.on_scroll, this.thread), 500 );
- },
});
'show_compact_message': this.action.params.view_mailbox ? false : 1,
'view_inbox': false,
'emails_from_on_composer': false,
+ 'fetch_limit': 1000 // allow inbox to load all children messages
}, this.action.params);
},
* @param {Object} defaults ??
*/
load_searchview: function (defaults) {
- var ds_msg = new session.web.DataSetSearch(this, 'mail.message');
- this.searchview = new session.web.SearchView(this, ds_msg, false, defaults || {}, false);
+ var self = this,
+ ds_msg = new session.web.DataSetSearch(this, 'mail.message'),
+ options = { $buttons: this.$('.oe-search-options') };
+ this.searchview = new session.web.SearchView(this, ds_msg, false, defaults || {}, options);
this.searchview.on('search_data', this, this.do_searchview_search);
- this.searchview.appendTo(this.$('.oe_view_manager_view_search'),
- this.$('.oe_searchview_drawer_container'));
+ this.searchview.appendTo(this.$('.oe-view-manager-search-view')).then(function () {
+ self.searchview.toggle_visibility(true);
+ });
if (this.searchview.has_defaults) {
this.searchview.ready.then(this.searchview.do_search);
}
'name': fields.char('Unit of Measure', required=True, translate=True),
'category_id': fields.many2one('product.uom.categ', 'Product Category', required=True, ondelete='cascade',
help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."),
- 'factor': fields.float('Ratio', required=True,digits=(12, 12),
+ 'factor': fields.float('Ratio', required=True, digits=0, # force NUMERIC with unlimited precision
help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category:\n'\
'1 * (reference unit) = ratio * (this unit)'),
- 'factor_inv': fields.function(_factor_inv, digits=(12,12),
+ 'factor_inv': fields.function(_factor_inv, digits=0, # force NUMERIC with unlimited precision
fnct_inv=_factor_inv_write,
string='Bigger Ratio',
help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category:\n'\
'warranty': fields.float('Warranty'),
'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
- 'state': fields.selection([('',''),
- ('draft', 'In Development'),
+ 'state': fields.selection([('draft', 'In Development'),
('sellable','Normal'),
('end','End of Lifecycle'),
('obsolete','Obsolete')], 'Status'),
''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
if isinstance(ids, (int, long)):
ids = [ids]
- if 'uom_po_id' in vals:
- new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)
- for product in self.browse(cr, uid, ids, context=context):
- old_uom = product.uom_po_id
- if old_uom.category_id.id != new_uom.category_id.id:
- raise osv.except_osv(_('Unit of Measure categories Mismatch!'), _("New Unit of Measure '%s' must belong to same Unit of Measure category '%s' as of old Unit of Measure '%s'. If you need to change the unit of measure, you may deactivate this product from the 'Procurements' tab and create a new one.") % (new_uom.name, old_uom.category_id.name, old_uom.name,))
if 'standard_price' in vals:
for prod_template_id in ids:
self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
<field name="arch" type="xml">
<search string="Product">
<field name="name" string="Product"/>
- <filter string="Services" icon="terp-accessories-archiver" domain="[('type','=','service')]"/>
+ <filter string="Services" name="services" domain="[('type','=','service')]"/>
<filter string="Consumable" name="consumable" icon="terp-accessories-archiver" domain="[('type','=','consu')]" help="Consumable products"/>
<separator/>
<filter string="Can be Sold" name="filter_to_sell" icon="terp-accessories-archiver-minus" domain="[('sale_ok','=',1)]"/>
<!-- product product -->
- <menuitem id="prod_config_main" name="Product Categories & Attributes" parent="base.menu_base_config" sequence="70" groups="base.group_no_one"/>
+ <menuitem id="prod_config_main" name="Products" parent="base.menu_base_config" sequence="2" groups="base.group_no_one"/>
<record id="product_product_tree_view" model="ir.ui.view">
<field name="name">product.product.tree</field>
<menuitem action="attribute_action"
id="menu_attribute_action"
- parent="product.prod_config_main" sequence="9" />
+ parent="product.prod_config_main" sequence="4" />
<record id="variants_tree_view" model="ir.ui.view">
<field name="name">product.attribute.value.tree</field>
<menuitem action="variants_action"
id="menu_variants_action"
- parent="product.prod_config_main" sequence="10" />
+ parent="product.prod_config_main" sequence="5" />
<!-- -->
<field name="model">product.category</field>
<field name="arch" type="xml">
<form string="Product Categories">
- <sheet>
- <div class="oe_title">
- <label for="name" class="oe_edit_only"/>
- <h1>
- <field name="name"/>
- </h1>
- </div>
- <group>
- <group name="parent" col="4">
- <field name="parent_id"/>
- <field name="type"/>
- </group>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only"/>
+ <h1>
+ <field name="name"/>
+ </h1>
+ </div>
+ <group>
+ <group name="parent" col="4">
+ <field name="parent_id"/>
+ <field name="type"/>
</group>
- </sheet>
+ </group>
</form>
</field>
</record>
parent="base.menu_product"
sequence="30" groups="base.group_no_one"/>
<record id="product_category_action_form" model="ir.actions.act_window">
- <field name="name">Product Categories</field>
+ <field name="name">Internal Categories</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.category</field>
<field name="view_type">form</field>
<field name="uom_type" on_change="onchange_type(uom_type)"/>
<label for="factor"/>
<div>
- <field name="factor" attrs="{'invisible':[('uom_type','!=','smaller')]}"/>
- <field name="factor_inv" attrs="{'invisible':[('uom_type','!=','bigger')]}"/>
+ <field name="factor"
+ digits="[42,5]"
+ attrs="{'invisible':[('uom_type','!=','smaller')],
+ 'readonly':[('uom_type','!=','smaller')]}"/>
+ <field name="factor_inv"
+ digits="[42,5]"
+ attrs="{'invisible':[('uom_type','!=','bigger')],
+ 'readonly':[('uom_type','!=','bigger')]}"/>
<p attrs="{'invisible':[('uom_type','!=','smaller')]}" class="oe_grey">
e.g: 1 * (reference unit) = ratio * (this unit)
</p>
</field>
</record>
<menuitem id="next_id_16" name="Units of Measure" parent="prod_config_main" sequence="30" groups="product.group_uom"/>
- <menuitem action="product_uom_form_action" id="menu_product_uom_form_action" parent="base.menu_base_config" sequence="30" groups="product.group_uom"/>
+ <menuitem action="product_uom_form_action" id="menu_product_uom_form_action" parent="product.prod_config_main" sequence="6" groups="product.group_uom"/>
<record id="product_uom_categ_form_view" model="ir.ui.view">
<field name="name">product.uom.categ.form</field>
</p>
</field>
</record>
- <menuitem action="product_uom_categ_form_action" id="menu_product_uom_categ_form_action" parent="base.menu_base_config" sequence="25" groups="base.group_no_one"/>
+ <menuitem action="product_uom_categ_form_action" id="menu_product_uom_categ_form_action" parent="product.prod_config_main" sequence="7" groups="base.group_no_one"/>
<record id="product_ul_form_view" model="ir.ui.view">
<field name="name">product.ul.form.view</field>
</field>
</record>
<menuitem
- action="product_ul_form_action" groups="product.group_stock_packaging" id="menu_product_ul_form_action" parent="prod_config_main" sequence="5"/>
+ action="product_ul_form_action" groups="product.group_stock_packaging" id="menu_product_ul_form_action" parent="prod_config_main" sequence="3"/>
<record id="product_packaging_tree_view" model="ir.ui.view">
<field name="name">product.packaging.tree.view</field>
_columns = {
'name': fields.char('Task Summary', readonly=True),
'user_id': fields.many2one('res.users', 'Assigned To', readonly=True),
- 'reviewer_id': fields.many2one('res.users', 'Reviewer', readonly=True),
'date_start': fields.datetime('Assignation Date', readonly=True),
'no_of_days': fields.integer('# of Days', size=128, readonly=True),
'date_end': fields.datetime('Ending Date', readonly=True),
t.date_deadline as date_deadline,
abs((extract('epoch' from (t.write_date-t.date_start)))/(3600*24)) as no_of_days,
t.user_id,
- t.reviewer_id,
progress as progress,
t.project_id,
t.effective_hours as hours_effective,
planned_hours as hours_planned,
(extract('epoch' from (t.write_date-t.create_date)))/(3600*24) as closing_days,
(extract('epoch' from (t.date_start-t.create_date)))/(3600*24) as opening_days,
- (extract('epoch' from (t.date_deadline-now())))/(3600*24) as delay_endings_days
+ (extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days
FROM project_task t
WHERE t.active = 'true'
GROUP BY
date_deadline,
date_last_stage_update,
t.user_id,
- t.reviewer_id,
t.project_id,
t.priority,
name,
self.module_list = all_modules;
var loaded = self.load_translations();
- var datejs_locale = "/web/static/lib/datejs/globalization/" + self.user_context.lang.replace("_", "-") + ".js";
-
- var file_list = [ datejs_locale ];
+ var locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US';
+ var file_list = [ locale ];
if(to_load.length) {
loaded = $.when(
loaded,
if (this.length) {
for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) {
attr = attrs.item(i);
- o[attr.nodeName] = attr.nodeValue;
+ o[attr.nodeName] = attr.value;
}
}
return o;
$.fn.tooltip.Constructor.DEFAULTS.container = 'body';
//overwrite bootstrap tooltip method to prevent showing 2 tooltip at the same time
var bootstrap_show_function = $.fn.tooltip.Constructor.prototype.show;
+ $.fn.modal.Constructor.prototype.enforceFocus = function () { };
$.fn.tooltip.Constructor.prototype.show = function () {
$('.tooltip').remove();
//the following fix the bug when using placement
this.model = model;
this.domain = domain;
this.mode = options.mode || 'pivot'; // pivot, bar, pie, line
- this.heatmap_mode = options.heatmap_mode || 'none';
this.visible_ui = options.visible_ui || true;
this.bar_ui = options.bar_ui || 'group';
this.graph_view = options.graph_view || null;
this.pivot_options = options;
this.title = options.title || 'Data';
+ this.$buttons = options.$buttons;
},
start: function() {
this.table = $('<table>');
this.$('.graph_main_content').append(this.table);
+ this.$buttons.find('.oe-pivot-mode').click(function () {
+ self.set_mode.bind(self)('pivot');
+ });
+ this.$measure_list = this.$buttons.find('.oe-measure-list');
+
+ this.$buttons.find('.oe-bar-mode').click(function () {
+ self.set_mode.bind(self)('bar');
+ });
+ this.$buttons.find('.oe-line-mode').click(function () {
+ self.set_mode.bind(self)('line');
+ });
+ this.$buttons.find('.oe-pie-mode').click(function () {
+ self.set_mode.bind(self)('pie');
+ });
+ this.$buttons.find('.fa-expand').click(this.swap_axis.bind(this));
+ this.$buttons.find('.fa-arrows-alt').click(function () {
+ self.pivot.expand_all().then(self.proxy('display_data'));
+ });
+ this.$buttons.find('.fa-download').click(this.export_xls.bind(this));
+
var indexes = {'pivot': 0, 'bar': 1, 'line': 2, 'chart': 3};
- this.$('.graph_mode_selection label').eq(indexes[this.mode]).addClass('active');
+ this.$('.graph_mode_selection label').eq(indexes[this.mode]).addClass('selected');
if (this.mode !== 'pivot') {
this.$('.graph_heatmap label').addClass('disabled');
} else {
this.$('.graph_main_content').addClass('graph_pivot_mode');
}
-
// get search view
var parent = this.getParent();
while (!(parent instanceof openerp.web.ViewManager)) {
return this.model.call('fields_get', []).then(function (f) {
self.fields = f;
- self.fields.__count = {field:'__count', type: 'integer', string:_t('Quantity')};
+ self.fields.__count = {field:'__count', type: 'integer', string:_t('Count')};
self.groupby_fields = self.get_groupby_fields();
self.measure_list = self.get_measures();
self.add_measures_to_options();
self.pivot_options.row_groupby = self.create_field_values(self.pivot_options.row_groupby || []);
self.pivot_options.col_groupby = self.create_field_values(self.pivot_options.col_groupby || []);
- self.pivot_options.measures = self.create_field_values(self.pivot_options.measures || [{field:'__count', type: 'integer', string:'Quantity'}]);
+ self.pivot_options.measures = self.create_field_values(self.pivot_options.measures || [{field:'__count', type: 'integer', string:'Count'}]);
self.pivot = new openerp.web_graph.PivotTable(self.model, self.domain, self.fields, self.pivot_options);
self.pivot.update_data().then(function () {
self.display_data();
// this method gets the fields that appear in the search view, under the
// 'Groupby' heading
get_search_fields: function () {
+ // this method is disabled for now. This requires extensive changes because the
+ // search view works quite differently: But the graph view is going to be split
+ // soon in pivot view and graph view. The pivot view will then properly handle
+ // groupbys.
+ return [];
var self = this;
var groupbygroups = _(this.search_view.drawer.inputs).select(function (g) {
},
add_measures_to_options: function() {
- this.$('.graph_measure_selection').append(
+ this.$measure_list.append(
_.map(this.measure_list, function (measure) {
return $('<li>').append($('<a>').attr('data-choice', measure.field)
.attr('href', '#')
.text(measure.string));
}));
+ this.$measure_list.find('li').click(this.measure_selection.bind(this));
},
// ----------------------------------------------------------------------
this.display_data();
},
- set_heatmap_mode: function (mode) { // none, row, col, all
- this.heatmap_mode = mode;
- if (mode === 'none') {
- this.$('.graph_heatmap label').removeClass('disabled');
- this.$('.graph_heatmap label').removeClass('active');
- }
- this.display_data();
- },
-
create_field_value: function (f) {
var field = (_.contains(f, ':')) ? f.split(':')[0] : f,
groupby_field = _.findWhere(this.groupby_fields, {field:field}),
put_measure_checkmarks: function () {
var self = this,
- measures_li = this.$('.graph_measure_selection a');
- measures_li.removeClass('oe_selected');
+ measures_li = this.$measure_list.find('li');
+ measures_li.removeClass('selected');
_.each(this.measure_list, function (measure, index) {
if (_.findWhere(self.pivot.measures, measure)) {
- measures_li.eq(index).addClass('oe_selected');
+ measures_li.eq(index).addClass('selected');
}
});
}
},
- heatmap_mode_selection: function (event) {
- event.preventDefault();
- var mode = event.currentTarget.getAttribute('data-mode');
- if (this.heatmap_mode === mode) {
- event.stopPropagation();
- this.set_heatmap_mode('none');
- } else {
- this.set_heatmap_mode(mode);
- }
- },
-
header_cell_clicked: function (event) {
event.preventDefault();
event.stopPropagation();
var formatted_value = raw && !_.isUndefined(value) ? value : openerp.web.format_value(value, {type:this.pivot.measures[index].type}),
cell = {value:formatted_value};
- if (this.heatmap_mode === 'none') { return cell; }
- var total = (this.heatmap_mode === 'both') ? this.pivot.get_total()[index]
- : (this.heatmap_mode === 'row') ? this.pivot.get_total(row)[index]
- : this.pivot.get_total(col)[index];
- var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
- if (color < 255) {
- cell.color = color;
- }
return cell;
},
this.$('.graph_main_content svg').remove();
this.$('.graph_main_content div').remove();
this.table.empty();
- this.table.toggleClass('heatmap', this.heatmap_mode !== 'none');
this.$('.graph_options_selection label').last().toggleClass('disabled', this.pivot.no_data);
this.width = this.$el.width();
this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
from openerp.tools import html_escape as escape, ustr, image_resize_and_sharpen, image_save_for_web
from openerp.tools.safe_eval import safe_eval
from openerp.addons.web.http import request
+from werkzeug.exceptions import NotFound
logger = logging.getLogger(__name__)
# NOTE: as the pattern is used as it for the ModelConverter (ir_http.py), do not use any flags
_UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
+DEFAULT_CDN_FILTERS = [
+ "^/[^/]+/static/",
+ "^/web/(css|js)/",
+ "^/website/image/",
+]
+
def unslug(s):
"""Extract slug and id from a string.
Always return un 2-tuple (str|None, int|None)
return werkzeug.Href(url)(params or None)
class website(osv.osv):
- def _get_menu_website(self, cr, uid, ids, context=None):
- # IF a menu is changed, update all websites
- return self.search(cr, uid, [], context=context)
-
def _get_menu(self, cr, uid, ids, name, arg, context=None):
- root_domain = [('parent_id', '=', False)]
- menus = self.pool.get('website.menu').search(cr, uid, root_domain, order='id', context=context)
- menu = menus and menus[0] or False
- return dict( map(lambda x: (x, menu), ids) )
+ res = {}
+ menu_obj = self.pool.get('website.menu')
+ for id in ids:
+ menu_ids = menu_obj.search(cr, uid, [('parent_id', '=', False), ('website_id', '=', id)], order='id', context=context)
+ res[id] = menu_ids and menu_ids[0] or False
+ return res
_name = "website" # Avoid website.website convention for conciseness (for new api). Got a special authorization from xmo and rco
_description = "Website"
_columns = {
- 'name': fields.char('Domain'),
+ 'name': fields.char('Website Name'),
+ 'domain': fields.char('Website Domain'),
'company_id': fields.many2one('res.company', string="Company"),
'language_ids': fields.many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages'),
'default_lang_id': fields.many2one('res.lang', string="Default language"),
'social_googleplus': fields.char('Google+ Account'),
'google_analytics_key': fields.char('Google Analytics Key'),
'user_id': fields.many2one('res.users', string='Public User'),
+ 'compress_html': fields.boolean('Compress HTML'),
+ 'cdn_activated': fields.boolean('Activate CDN for assets'),
+ 'cdn_url': fields.char('CDN Base URL'),
+ 'cdn_filters': fields.text('CDN Filters', help="URL matching those filters will be rewritten using the CDN Base URL"),
'partner_id': fields.related('user_id','partner_id', type='many2one', relation='res.partner', string='Public Partner'),
- 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu',
- store= {
- 'website.menu': (_get_menu_website, ['sequence','parent_id','website_id'], 10)
- })
+ 'menu_id': fields.function(_get_menu, relation='website.menu', type='many2one', string='Main Menu')
}
-
_defaults = {
- 'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
+ 'user_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID, 'base.public_user'),
+ 'company_id': lambda self,cr,uid,c: self.pool['ir.model.data'].xmlid_to_res_id(cr, openerp.SUPERUSER_ID,'base.main_company'),
+ 'compress_html': False,
+ 'cdn_activated': False,
+ 'cdn_url': '//localhost:8069/',
+ 'cdn_filters': '\n'.join(DEFAULT_CDN_FILTERS),
}
# cf. Wizard hack in website_views.xml
except ValueError:
# new page
_, template_id = imd.get_object_reference(cr, uid, template_module, template_name)
- page_id = view.copy(cr, uid, template_id, context=context)
+ website_id = context.get('website_id')
+ key = template_module+'.'+page_name
+ page_id = view.copy(cr, uid, template_id, {'website_id': website_id, 'key': key}, context=context)
page = view.browse(cr, uid, page_id, context=context)
page.write({
'arch': page.arch.replace(template, page_xmlid),
'name': page_name,
'page': ispage,
})
- imd.create(cr, uid, {
- 'name': page_name,
- 'module': template_module,
- 'model': 'ir.ui.view',
- 'res_id': page_id,
- 'noupdate': True
- }, context=context)
return page_xmlid
def page_for_name(self, cr, uid, ids, name, module='website', context=None):
website = self.browse(cr, uid, id)
return [(lg.code, lg.name) for lg in website.language_ids]
+ def get_cdn_url(self, cr, uid, uri, context=None):
+ # Currently only usable in a website_enable request context
+ if request and request.website and not request.debug:
+ cdn_url = request.website.cdn_url
+ cdn_filters = (request.website.cdn_filters or '').splitlines()
+ for flt in cdn_filters:
+ if flt and re.match(flt, uri):
+ return urlparse.urljoin(cdn_url, uri)
+ return uri
+
def get_languages(self, cr, uid, ids, context=None):
return self._get_languages(cr, uid, ids[0], context=context)
lang['hreflang'] = lang['short']
return langs
+ @openerp.tools.ormcache(skiparg=4)
+ def _get_current_website_id(self, cr, uid, domain_name, context=None):
+ website_id = 1
+ if request:
+ ids = self.search(cr, uid, [('domain', '=', domain_name)], context=context)
+ if ids:
+ website_id = ids[0]
+ return website_id
+
def get_current_website(self, cr, uid, context=None):
- # TODO: Select website, currently hard coded
- return self.pool['website'].browse(cr, uid, 1, context=context)
+ domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
+ website_id = self._get_current_website_id(cr, uid, domain_name, context=context)
+ return self.browse(cr, uid, website_id, context=context)
def is_publisher(self, cr, uid, ids, context=None):
Access = self.pool['ir.model.access']
return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context)
def get_template(self, cr, uid, ids, template, context=None):
- if isinstance(template, (int, long)):
- view_id = template
- else:
- if '.' not in template:
- template = 'website.%s' % template
- module, xmlid = template.split('.', 1)
- model, view_id = request.registry["ir.model.data"].get_object_reference(cr, uid, module, xmlid)
- return self.pool["ir.ui.view"].browse(cr, uid, view_id, context=context)
+ if not isinstance(template, (int, long)) and '.' not in template:
+ template = 'website.%s' % template
+ View = self.pool['ir.ui.view']
+ view_id = View.get_view_id(cr, uid, template, context=context)
+ if not view_id:
+ raise NotFound
+ return View.browse(cr, uid, view_id, context=context)
def _render(self, cr, uid, ids, template, values=None, context=None):
# TODO: remove this. (just kept for backward api compatibility for saas-3)
'zoom': zoom,
'sensor': 'false',
}
- return urlplus('http://maps.googleapis.com/maps/api/staticmap' , params)
+ return urlplus('//maps.googleapis.com/maps/api/staticmap' , params)
def google_map_link(self, cr, uid, ids, zoom=8, context=None):
partner = self.browse(cr, uid, ids[0], context=context)
<!-- Layout and generic templates -->
-<template id="website.theme" name="Theme">
- <link id="bootstrap_css" rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css' t-ignore="true"/>
- <link rel="stylesheet" href='/website/static/src/css/website.css' t-ignore="true"/>
-</template>
-
<template id="website.assets_frontend" name="Website assets">
<t t-call="website.theme"/>
<script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script>
-
+
</template>
<template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend">
<t t-set="additional_title" t-value="main_object.name"/>
</t>
<t t-if="not title">
- <t t-set="title"><t t-raw="res_company.name"/><t t-if="additional_title"> - <t t-raw="additional_title"/></t></t>
+ <t t-set="title"><t t-if="additional_title"><t t-raw="additional_title"/> | </t><t t-esc="(website or res_company).name"/></t>
</t>
<meta name="viewport" content="initial-scale=1"/>
<meta name="description" t-att-content="main_object and 'website_meta_description' in main_object
<t t-call-assets="website.assets_frontend" t-js="false"/>
<t t-raw="head or ''" name='layout_head'/>
- <t t-if="website and website.google_analytics_key">
- <script>
- (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
- (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
- m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
- })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-
- ga('create', _.str.trim('<t t-esc="website.google_analytics_key"/>'), 'auto');
- ga('send','pageview');
- </script>
- </t>
</head>
<body>
<div id="wrapwrap">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
- <a class="navbar-brand" href="/" t-field="res_company.name"/>
+ <a class="navbar-brand" href="/">YourCompany</a>
</div>
<div class="collapse navbar-collapse navbar-top-collapse">
<ul class="nav navbar-nav navbar-right" id="top_menu">
<li class="dropdown" t-ignore="true" t-if="website.user_id != user_id">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<b>
- <span t-esc="user_id.name"/>
+ <span t-esc="(len(user_id.name)>25) and (user_id.name[:23]+'...') or user_id.name"/>
<span class="caret"></span>
</b>
</a>
<ul class="dropdown-menu js_usermenu" role="menu">
- <li><a href="/web" role="menuitem">My Account</a></li>
- <li class="divider"/>
<li><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li>
</ul>
</li>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="website.assets_frontend" t-css="false"/>
+ <script t-if="website and website.google_analytics_key">
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+ ga('create', _.str.trim('<t t-esc="website.google_analytics_key"/>'), 'auto');
+ ga('send','pageview');
+ </script>
</body>
</html>
</template>
<script type="text/javascript" src="/website/static/src/js/website.tour.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script> <!-- groups="base.group_website_designer" -->
<script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script>
+ <script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js" />
<script type="text/javascript" src="/website/static/src/js/website.ace.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.translator.js"></script>
+ <script type="text/javascript" src="/website/static/src/js/website.theme.js"></script>
</template>
<template id="footer_custom" inherit_id="website.layout" name="Footer">
<xpath expr="//div[@id='footer_container']" position="replace">
<div class="oe_structure" id="footer">
- <section data-snippet-id='three-columns' class="mt16 mb16">
+ <section class="mt16 mb16">
<div class="container">
<div class="row">
<div class="col-md-4">
<div id="wrap">
<div class="oe_structure">
- <section data-snippet-id="title">
+ <section>
<div class="container">
<div class="row">
<div class="col-md-12">
</div>
</section>
- <section data-snippet-id="text-image">
+ <section>
<div class="container">
<div class="row">
<div class="col-md-6 mt32">
# -*- coding: utf-8 -*-
- from datetime import datetime
import werkzeug.urls
import werkzeug.wrappers
- import re
import simplejson
+import lxml
+from urllib2 import urlopen
from openerp import tools
from openerp import SUPERUSER_ID
from openerp.addons.web.http import request
from openerp.addons.website.controllers.main import Website as controllers
from openerp.addons.website.models.website import slug
- from openerp.tools.translate import _
controllers = controllers()
def _prepare_forum_values(self, forum=None, **kwargs):
user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context)
- values = {'user': user,
- 'is_public_user': user.id == request.website.user_id.id,
- 'notifications': self._get_notifications(),
- 'header': kwargs.get('header', dict()),
- 'searches': kwargs.get('searches', dict()),
- 'no_introduction_message': request.httprequest.cookies.get('no_introduction_message', False),
- }
+ values = {
+ 'user': user,
+ 'is_public_user': user.id == request.website.user_id.id,
+ 'notifications': self._get_notifications(),
+ 'header': kwargs.get('header', dict()),
+ 'searches': kwargs.get('searches', dict()),
++ 'no_introduction_message': request.httprequest.cookies.get('no_introduction_message', False),
+ 'validation_email_sent': request.session.get('validation_email_sent', False),
+ 'validation_email_done': request.session.get('validation_email_done', False),
+ }
if forum:
values['forum'] = forum
elif kwargs.get('forum_id'):
values.update(kwargs)
return values
+ # User and validation
+ # --------------------------------------------------
+
+ @http.route('/forum/send_validation_email', type='json', auth='user', website=True)
+ def send_validation_email(self, forum_id=None, **kwargs):
+ request.registry['res.users'].send_forum_validation_email(request.cr, request.uid, request.uid, forum_id=forum_id, context=request.context)
+ request.session['validation_email_sent'] = True
+ return True
+
+ @http.route('/forum/validate_email', type='http', auth='public', website=True)
+ def validate_email(self, token, id, email, forum_id=None, **kwargs):
+ if forum_id:
+ try:
+ forum_id = int(forum_id)
+ except ValueError:
+ forum_id = None
+ done = request.registry['res.users'].process_forum_validation_token(request.cr, request.uid, token, int(id), email, forum_id=forum_id, context=request.context)
+ if done:
+ request.session['validation_email_done'] = True
+ if forum_id:
+ return request.redirect("/forum/%s" % int(forum_id))
+ return request.redirect('/forum')
+
+ @http.route('/forum/validate_email/close', type='json', auth='public', website=True)
+ def validate_email_done(self):
+ request.session['validation_email_done'] = False
+ return True
+
# Forum
# --------------------------------------------------
'''/forum/<model("forum.forum"):forum>/tag/<model("forum.tag", "[('forum_id','=',forum[0])]"):tag>/questions''',
'''/forum/<model("forum.forum"):forum>/tag/<model("forum.tag", "[('forum_id','=',forum[0])]"):tag>/questions/page/<int:page>''',
], type='http', auth="public", website=True)
- def questions(self, forum, tag=None, page=1, filters='all', sorting='date', search='', **post):
+ def questions(self, forum, tag=None, page=1, filters='all', sorting=None, search='', post_type=None, **post):
cr, uid, context = request.cr, request.uid, request.context
Post = request.registry['forum.post']
user = request.registry['res.users'].browse(cr, uid, uid, context=context)
domain += [('child_ids', '=', False)]
elif filters == 'followed':
domain += [('message_follower_ids', '=', user.partner_id.id)]
- else:
- filters = 'all'
-
- if sorting == 'answered':
- order = 'child_count desc'
- elif sorting == 'vote':
- order = 'vote_count desc'
- elif sorting == 'date':
- order = 'write_date desc'
- else:
- sorting = 'creation'
- order = 'create_date desc'
+
+ if post_type:
+ domain += [('type', '=', post_type)]
+ if not sorting:
+ sorting = forum.default_order
question_count = Post.search(cr, uid, domain, count=True, context=context)
if tag:
else:
url = "/forum/%s" % slug(forum)
- url_args = {}
+ url_args = {
+ 'sorting': sorting
+ }
if search:
url_args['search'] = search
if filters:
url_args['filters'] = filters
- if sorting:
- url_args['sorting'] = sorting
pager = request.website.pager(url=url, total=question_count, page=page,
step=self._post_per_page, scope=self._post_per_page,
url_args=url_args)
- obj_ids = Post.search(cr, uid, domain, limit=self._post_per_page, offset=pager['offset'], order=order, context=context)
+ obj_ids = Post.search(cr, uid, domain, limit=self._post_per_page, offset=pager['offset'], order=sorting, context=context)
question_ids = Post.browse(cr, uid, obj_ids, context=context)
values = self._prepare_forum_values(forum=forum, searches=post)
'filters': filters,
'sorting': sorting,
'search': search,
+ 'post_type': post_type,
})
return request.website.render("website_forum.forum_index", values)
# Questions
# --------------------------------------------------
- @http.route(['/forum/<model("forum.forum"):forum>/ask'], type='http', auth="public", website=True)
- def question_ask(self, forum, **post):
- if not request.session.uid:
- return login_redirect()
- values = self._prepare_forum_values(forum=forum, searches={}, header={'ask_hide': True})
- return request.website.render("website_forum.ask_question", values)
-
- @http.route('/forum/<model("forum.forum"):forum>/question/new', type='http', auth="user", methods=['POST'], website=True)
- def question_create(self, forum, **post):
- cr, uid, context = request.cr, request.uid, request.context
- Tag = request.registry['forum.tag']
- question_tag_ids = []
- if post.get('question_tags').strip('[]'):
- tags = post.get('question_tags').strip('[]').replace('"', '').split(",")
- for tag in tags:
- tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context)
- if tag_ids:
- question_tag_ids.append((4, tag_ids[0]))
- else:
- question_tag_ids.append((0, 0, {'name': tag, 'forum_id': forum.id}))
-
- new_question_id = request.registry['forum.post'].create(
- request.cr, request.uid, {
- 'forum_id': forum.id,
- 'name': post.get('question_name'),
- 'content': post.get('content'),
- 'tag_ids': question_tag_ids,
- }, context=context)
- return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), new_question_id))
+ @http.route('/forum/get_url_title', type='json', auth="user", methods=['POST'], website=True)
+ def get_url_title(self, **kwargs):
+ arch = lxml.html.parse(urlopen(kwargs.get('url')))
+ return arch.find(".//title").text
@http.route(['''/forum/<model("forum.forum"):forum>/question/<model("forum.post", "[('forum_id','=',forum[0]),('parent_id','=',False)]"):question>'''], type='http', auth="public", website=True)
def question(self, forum, question, **post):
'forum': forum,
'reasons': reasons,
})
- return request.website.render("website_forum.close_question", values)
+ return request.website.render("website_forum.close_post", values)
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/edit_answer', type='http', auth="user", website=True)
def question_edit_answer(self, forum, question, **kwargs):
# Post
# --------------------------------------------------
- @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/new', type='http', auth="public", methods=['POST'], website=True)
- def post_new(self, forum, post, **kwargs):
+ @http.route(['/forum/<model("forum.forum"):forum>/<post_type>'], type='http', auth="public", website=True)
+ def forum_post(self, forum, post_type, **post):
if not request.session.uid:
return login_redirect()
cr, uid, context = request.cr, request.uid, request.context
user = request.registry['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
if not user.email or not tools.single_email_re.match(user.email):
return werkzeug.utils.redirect("/forum/%s/user/%s/edit?email_required=1" % (slug(forum), uid))
- request.registry['forum.post'].create(
- request.cr, request.uid, {
+ values = self._prepare_forum_values(forum=forum, searches={}, header={'ask_hide': True})
+ return request.website.render("website_forum.%s" % post_type, values)
+
+ @http.route(['/forum/<model("forum.forum"):forum>/<post_type>/new',
+ '/forum/<model("forum.forum"):forum>/<model("forum.post"):post_parent>/reply']
+ , type='http', auth="public", methods=['POST'], website=True)
+ def post_create(self, forum, post_parent='', post_type='', **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ if not request.session.uid:
+ return login_redirect()
+
+ post_tag_ids = []
+ Tag = request.registry['forum.tag']
+ if post.get('post_tags', False) and post.get('post_tags').strip('[]'):
+ tags = post.get('post_tags').strip('[]').replace('"', '').split(",")
+ for tag in tags:
+ tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context)
+ if tag_ids:
+ post_tag_ids.append((4, tag_ids[0]))
+ else:
+ post_tag_ids.append((0, 0, {'name': tag, 'forum_id': forum.id}))
+
+ new_question_id = request.registry['forum.post'].create(cr, uid, {
'forum_id': forum.id,
- 'parent_id': post.id,
- 'content': kwargs.get('content'),
- }, context=request.context)
- return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(post)))
+ 'name': post.get('post_name', ''),
+ 'content': post.get('content', False),
+ 'content_link': post.get('content_link', False),
+ 'parent_id': post_parent and post_parent.id or False,
+ 'tag_ids': post_tag_ids,
+ 'type': post_parent and post_parent.type or post_type,
+ }, context=context)
+ return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), post_parent and slug(post_parent) or new_question_id))
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment', type='http', auth="public", methods=['POST'], website=True)
def post_comment(self, forum, post, **kwargs):
cr, uid, context = request.cr, request.uid, request.context
if kwargs.get('comment') and post.forum_id.id == forum.id:
# TDE FIXME: check that post_id is the question or one of its answers
- request.registry['forum.post']._post_comment(
- cr, uid, post,
+ request.registry['forum.post'].message_post(
+ cr, uid, post.id,
body=kwargs.get('comment'),
- context=context)
+ type='comment',
+ subtype='mt_comment',
+ context=dict(context, mail_create_nosubcribe=True))
return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/save', type='http', auth="user", methods=['POST'], website=True)
def post_save(self, forum, post, **kwargs):
cr, uid, context = request.cr, request.uid, request.context
- question_tags = []
- if kwargs.get('question_tag') and kwargs.get('question_tag').strip('[]'):
+ post_tags = []
+ if kwargs.get('post_tag') and kwargs.get('post_tag').strip('[]'):
Tag = request.registry['forum.tag']
- tags = kwargs.get('question_tag').strip('[]').replace('"', '').split(",")
+ tags = kwargs.get('post_tag').strip('[]').replace('"', '').split(",")
for tag in tags:
tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context)
if tag_ids:
- question_tags += tag_ids
+ post_tags += tag_ids
else:
new_tag = Tag.create(cr, uid, {'name': tag, 'forum_id': forum.id}, context=context)
- question_tags.append(new_tag)
+ post_tags.append(new_tag)
vals = {
- 'tag_ids': [(6, 0, question_tags)],
- 'name': kwargs.get('question_name'),
+ 'tag_ids': [(6, 0, post_tags)],
+ 'name': kwargs.get('post_name'),
'content': kwargs.get('content'),
}
request.registry['forum.post'].write(cr, uid, [post.id], vals, context=context)
<!-- Reasons for closing Post -->
<record id="reason_1" model="forum.post.reason">
- <field name="name">duplicate question</field>
+ <field name="name">duplicate post</field>
</record>
<record id="reason_2" model="forum.post.reason">
<field name="name">off-topic or not relevant</field>
<field name="name">too subjective and argumentative</field>
</record>
<record id="reason_4" model="forum.post.reason">
- <field name="name">not a real question</field>
+ <field name="name">not a real post</field>
</record>
<record id="reason_6" model="forum.post.reason">
<field name="name">not relevant or out dated</field>
<field name="name">too localized</field>
</record>
+ <!-- Email template for email validation (for karma purpose) -->
+ <record id="validation_email" model="email.template">
+ <field name="name">Email Verification</field>
+ <field name="model_id" ref="base.model_res_users"/>
+ <field name="email_from"><![CDATA[${object.company_id.name} <${(object.company_id.email or user.email)|safe}>]]></field>
+ <field name="email_to">${object.email|safe}</field>
+ <field name="subject"><![CDATA[${object.company_id.name} Forums validation]]></field>
+ <field name="body_html"><![CDATA[
+ <p>
+ Hello ${object.name},
+ </p>
+ <p>
+ You have been invited to validate your email in order to get access to "${object.company_id.name}" Q/A Forums.
+ </p>
+ <p>
+ To validate your email, please click on the following link:
+ </p>
+ <ul>
+ <li><a href="${ctx.get('token_url')}">Validate my account for "${object.company_id.name}" Q/A Forums</a></li>
+ </ul>
+ <p>
+ Thanks,
+ </p>
+ <pre>
+ --
+ ${object.company_id.name or ''}
+ ${object.company_id.email or ''}
+ ${object.company_id.phone or ''}
+ </pre>]]></field>
+ </record>
+
</data>
</openerp>
# -*- coding: utf-8 -*-
from datetime import datetime
+ import uuid
+ from werkzeug.exceptions import Forbidden
import openerp
- from openerp import tools
+ from openerp import api, tools
from openerp import SUPERUSER_ID
from openerp.addons.website.models.website import slug
+ from openerp.exceptions import Warning
from openerp.osv import osv, fields
from openerp.tools import html2plaintext
from openerp.tools.translate import _
- from werkzeug.exceptions import Forbidden
class KarmaError(Forbidden):
""" Karma-related error, used for forum and posts. """
_description = 'Forums'
_inherit = ['mail.thread', 'website.seo.metadata']
+ def init(self, cr):
+ """ Add forum uuid for user email validation. """
+ forum_uuids = self.pool['ir.config_parameter'].search(cr, SUPERUSER_ID, [('key', '=', 'website_forum.uuid')])
+ if not forum_uuids:
+ self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'website_forum.uuid', str(uuid.uuid4()), ['base.group_system'])
+
_columns = {
- 'name': fields.char('Name', required=True, translate=True),
+ 'name': fields.char('Forum Name', required=True, translate=True),
'faq': fields.html('Guidelines'),
'description': fields.html('Description'),
+ 'introduction_message': fields.html('Introduction Message'),
+ 'relevancy_option_first': fields.float('First Relevancy Parameter'),
+ 'relevancy_option_second': fields.float('Second Relevancy Parameter'),
+ 'default_order': fields.selection([
+ ('create_date desc','Newest'),
+ ('write_date desc','Last Updated'),
+ ('vote_count desc','Most Voted'),
+ ('relevancy desc','Relevancy'),
+ ('child_count desc','Answered'),
+ ], 'Default Order', required=True),
+ 'default_allow': fields.selection([('post_link','Link'),('ask_question','Question'),('post_discussion','Discussion')], 'Default Post', required=True),
+ 'allow_link': fields.boolean('Links', help="When clicking on the post, it redirects to an external link"),
+ 'allow_question': fields.boolean('Questions', help="Users can answer only once per question. Contributors can edit answers and mark the right ones."),
+ 'allow_discussion': fields.boolean('Discussions'),
# karma generation
- 'karma_gen_question_new': fields.integer('Post a Questions'),
- 'karma_gen_question_upvote': fields.integer('Upvote a Question'),
- 'karma_gen_question_downvote': fields.integer('Downvote a Question'),
- 'karma_gen_answer_upvote': fields.integer('Upvote an Answer'),
- 'karma_gen_answer_downvote': fields.integer('Downvote an answer'),
- 'karma_gen_answer_accept': fields.integer('Accept an Answer'),
- 'karma_gen_answer_accepted': fields.integer('Have Your Answer Accepted'),
- 'karma_gen_answer_flagged': fields.integer('Have Your Answer Flagged'),
+ 'karma_gen_question_new': fields.integer('Asking a question'),
+ 'karma_gen_question_upvote': fields.integer('Question upvoted'),
+ 'karma_gen_question_downvote': fields.integer('Question downvoted'),
+ 'karma_gen_answer_upvote': fields.integer('Answer upvoted'),
+ 'karma_gen_answer_downvote': fields.integer('Answer downvoted'),
+ 'karma_gen_answer_accept': fields.integer('Accepting an answer'),
+ 'karma_gen_answer_accepted': fields.integer('Answer accepted'),
+ 'karma_gen_answer_flagged': fields.integer('Answer flagged'),
# karma-based actions
- 'karma_ask': fields.integer('Ask a question'),
+ 'karma_ask': fields.integer('Ask a new question'),
'karma_answer': fields.integer('Answer a question'),
'karma_edit_own': fields.integer('Edit its own posts'),
'karma_edit_all': fields.integer('Edit all posts'),
'karma_upvote': fields.integer('Upvote'),
'karma_downvote': fields.integer('Downvote'),
'karma_answer_accept_own': fields.integer('Accept an answer on its own questions'),
- 'karma_answer_accept_all': fields.integer('Accept an answers to all questions'),
+ 'karma_answer_accept_all': fields.integer('Accept an answer to all questions'),
'karma_editor_link_files': fields.integer('Linking files (Editor)'),
- 'karma_editor_clickable_link': fields.integer('Clickable links (Editor)'),
+ 'karma_editor_clickable_link': fields.integer('Add clickable links (Editor)'),
'karma_comment_own': fields.integer('Comment its own posts'),
'karma_comment_all': fields.integer('Comment all posts'),
'karma_comment_convert_own': fields.integer('Convert its own answers to comments and vice versa'),
- 'karma_comment_convert_all': fields.integer('Convert all answers to answers and vice versa'),
+ 'karma_comment_convert_all': fields.integer('Convert all answers to comments and vice versa'),
'karma_comment_unlink_own': fields.integer('Unlink its own comments'),
'karma_comment_unlink_all': fields.integer('Unlink all comments'),
'karma_retag': fields.integer('Change question tags'),
return False
_defaults = {
+ 'default_order': 'write_date desc',
+ 'allow_question': True,
+ 'default_allow': 'ask_question',
+ 'allow_link': False,
+ 'allow_discussion': False,
'description': 'This community is for professionals and enthusiasts of our products and services.',
'faq': _get_default_faq,
+ 'karma_gen_question_new': 0, # set to null for anti spam protection
+ 'introduction_message': """<h1 class="mt0">Welcome!</h1>
+ <p> This community is for professionals and enthusiasts of our products and services.
+ Share and discuss the best content and new marketing ideas,
+ build your professional profile and become a better marketer together.
+ </p>""",
+ 'relevancy_option_first': 0.8,
+ 'relevancy_option_second': 1.8,
- 'karma_gen_question_new': 2,
'karma_gen_question_upvote': 5,
'karma_gen_question_downvote': -2,
'karma_gen_answer_upvote': 10,
'karma_gen_answer_accept': 2,
'karma_gen_answer_accepted': 15,
'karma_gen_answer_flagged': -100,
- 'karma_ask': 0,
- 'karma_answer': 0,
+ 'karma_ask': 3, # set to not null for anti spam protection
+ 'karma_answer': 3, # set to not null for anti spam protection
'karma_edit_own': 1,
'karma_edit_all': 300,
'karma_close_own': 100,
'karma_answer_accept_all': 500,
'karma_editor_link_files': 20,
'karma_editor_clickable_link': 20,
- 'karma_comment_own': 1,
- 'karma_comment_all': 1,
+ 'karma_comment_own': 3,
+ 'karma_comment_all': 5,
'karma_comment_convert_own': 50,
'karma_comment_convert_all': 500,
'karma_comment_unlink_own': 50,
_inherit = ['mail.thread', 'website.seo.metadata']
_order = "is_correct DESC, vote_count DESC, write_date DESC"
+ def _get_post_relevancy(self, cr, uid, ids, field_name, arg, context):
+ res = dict.fromkeys(ids, 0)
+ for post in self.browse(cr, uid, ids, context=context):
+ days = (datetime.today() - datetime.strptime(post.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days
+ relavency = abs(post.vote_count - 1) ** post.forum_id.relevancy_option_first / ( days + 2) ** post.forum_id.relevancy_option_second
+ res[post.id] = relavency if (post.vote_count - 1) >= 0 else -relavency
+ return res
+
def _get_user_vote(self, cr, uid, ids, field_name, arg, context):
res = dict.fromkeys(ids, 0)
vote_ids = self.pool['forum.post.vote'].search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
'name': fields.char('Title'),
'forum_id': fields.many2one('forum.forum', 'Forum', required=True),
'content': fields.html('Content'),
+ 'content_link': fields.char('URL', help="URL of Link Articles"),
'tag_ids': fields.many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', 'Tags'),
'state': fields.selection([('active', 'Active'), ('close', 'Close'), ('offensive', 'Offensive')], 'Status'),
'views': fields.integer('Number of Views'),
'active': fields.boolean('Active'),
+ 'type': fields.selection([('question', 'Question'), ('link', 'Article'), ('discussion', 'Discussion')], 'Type'),
+ 'relevancy': fields.function(
+ _get_post_relevancy, string="Relevancy", type='float',
+ store={
+ 'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['vote_ids'], 10),
+ 'forum.post.vote': (_get_post_from_vote, [], 10),
+ }),
'is_correct': fields.boolean('Valid Answer', help='Correct Answer or Answer on this question accepted.'),
'website_message_ids': fields.one2many(
'mail.message', 'res_id',
'state': 'active',
'views': 0,
'active': True,
+ 'type': 'question',
'vote_ids': list(),
'favourite_ids': list(),
'child_ids': list(),
create_context = dict(context, mail_create_nolog=True)
post_id = super(Post, self).create(cr, uid, vals, context=create_context)
post = self.browse(cr, uid, post_id, context=context)
+ # deleted or closed questions
+ if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active == False):
+ osv.except_osv(_('Error !'), _('Posting answer on [Deleted] or [Closed] question is prohibited'))
# karma-based access
if not post.parent_id and not post.can_ask:
raise KarmaError('Not enough karma to create a new question')
return super(Post, self).unlink(cr, uid, ids, context=context)
def vote(self, cr, uid, ids, upvote=True, context=None):
- posts = self.browse(cr, uid, ids, context=context)
-
- if upvote and any(not post.can_upvote for post in posts):
- raise KarmaError('Not enough karma to upvote.')
- elif not upvote and any(not post.can_downvote for post in posts):
- raise KarmaError('Not enough karma to downvote.')
-
Vote = self.pool['forum.post.vote']
vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
new_vote = '1' if upvote else '-1'
res_id = post.parent_id and "%s#answer-%s" % (post.parent_id.id, post.id) or post.id
return "/forum/%s/question/%s" % (post.forum_id.id, res_id)
- def _post_comment(self, cr, uid, post, body, context=None):
- context = dict(context or {}, mail_create_nosubcribe=True)
- if not post.can_comment:
- raise KarmaError('Not enough karma to comment')
- return self.message_post(cr, uid, post.id,
- body=body,
- type='comment',
- subtype='mt_comment',
- context=context)
+ @api.cr_uid_ids_context
+ def message_post(self, cr, uid, thread_id, type='notification', subtype=None, context=None, **kwargs):
+ if thread_id and type == 'comment': # user comments have a restriction on karma
+ if isinstance(thread_id, (list, tuple)):
+ post_id = thread_id[0]
+ else:
+ post_id = thread_id
+ post = self.browse(cr, uid, post_id, context=context)
+ if not post.can_comment:
+ raise KarmaError('Not enough karma to comment')
+ return super(Post, self).message_post(cr, uid, thread_id, type=type, subtype=subtype, context=context, **kwargs)
+
class PostReason(osv.Model):
_name = "forum.post.reason"
def create(self, cr, uid, vals, context=None):
vote_id = super(Vote, self).create(cr, uid, vals, context=context)
vote = self.browse(cr, uid, vote_id, context=context)
+
+ # own post check
+ if vote.user_id.id == vote.post_id.create_uid.id:
+ raise Warning('Not allowed to vote for its own post')
+ # karma check
+ if vote.vote == '1' and not vote.post_id.can_upvote:
+ raise KarmaError('Not enough karma to upvote.')
+ elif vote.vote == '-1' and not vote.post_id.can_downvote:
+ raise KarmaError('Not enough karma to downvote.')
+
+ # karma update
if vote.post_id.parent_id:
karma_value = self._get_karma_value('0', vote.vote, vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote)
else:
def write(self, cr, uid, ids, values, context=None):
if 'vote' in values:
for vote in self.browse(cr, uid, ids, context=context):
+ # own post check
+ if vote.user_id.id == vote.post_id.create_uid.id:
+ raise Warning('Not allowed to vote for its own post')
+ # karma check
+ if (values['vote'] == '1' or vote.vote == '-1' and values['vote'] == '0') and not vote.post_id.can_upvote:
+ raise KarmaError('Not enough karma to upvote.')
+ elif (values['vote'] == '-1' or vote.vote == '1' and values['vote'] == '0') and not vote.post_id.can_downvote:
+ raise KarmaError('Not enough karma to downvote.')
+
+ # karma update
if vote.post_id.parent_id:
karma_value = self._get_karma_value(vote.vote, values['vote'], vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote)
else:
ev.preventDefault();
var $warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="karma_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">×</button>'+
- karma + ' karma is required to perform this action. You can earn karma by answering questions or having '+
- 'your answers upvoted by the community.</div>');
+ karma + ' karma is required to perform this action. You can earn karma by having '+
+ 'your answers upvoted by the community.</div>');
var vote_alert = $(ev.currentTarget).parent().find("#vote_alert");
if (vote_alert.length == 0) {
$(ev.currentTarget).parent().append($warning);
}
}
});
- return true;
});
$('.accept_answer').not('.karma_required').on('click', function (ev) {
}
}
});
- return true;
});
$('.favourite_question').on('click', function (ev) {
$link.removeClass("forum_favourite_question")
}
});
- return true;
});
$('.comment_delete').on('click', function (ev) {
openerp.jsonRpc($link.data('href'), 'call', {}).then(function (data) {
$link.parents('.comment').first().remove();
});
- return true;
});
$('.notification_close').on('click', function (ev) {
ev.preventDefault();
var $link = $(ev.currentTarget);
openerp.jsonRpc("/forum/notification_read", 'call', {
- 'notification_id': $link.attr("id")})
- return true;
+ 'notification_id': $link.attr("id")});
+ });
+
+ $('.send_validation_email').on('click', function (ev) {
+ ev.preventDefault();
+ var $link = $(ev.currentTarget);
+ openerp.jsonRpc("/forum/send_validation_email", 'call', {
+ 'forum_id': $link.attr('forum-id'),
+ }).then(function (data) {
+ if (data) {
+ $('button.validation_email_close').click();
+ }
+ });
+ });
+
+ $('.validated_email_close').on('click', function (ev) {
+ openerp.jsonRpc("/forum/validate_email/close", 'call', {});
});
+ $('.js_close_intro').on('click', function (ev) {
+ ev.preventDefault();
+ document.cookie = "no_introduction_message = false";
+ return true;
+ });
+
+ $('.link_url').on('change', function (ev) {
+ ev.preventDefault();
+ var $link = $(ev.currentTarget);
+ if ($link.attr("value").search("^http(s?)://.*")) {
+ var $warning = $('<div class="alert alert-danger alert-dismissable" style="position:absolute; margin-top: -180px; margin-left: 90px;">'+
+ '<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">×</button>'+
+ 'Please enter valid URl.'+
+ '</div>');
+ $link.parent().append($warning);
+ $link.parent().find("button#btn_post_your_article")[0].disabled = true;
+ $link.parent().find("input[name='content']")[0].value = '';
+ } else {
+ openerp.jsonRpc("/forum/get_url_title", 'call', {'url': $link.attr("value")}).then(function (data) {
+ $link.parent().find("input[name='content']")[0].value = data;
+ $('button').prop('disabled', false);
+ $('input').prop('readonly', false);
+ });
+ }
+ });
+
if($('input.load_tags').length){
var tags = $("input.load_tags").val();
$("input.load_tags").val("");
function set_tags(tags) {
$("input.load_tags").textext({
plugins: 'tags focus autocomplete ajax',
+ ext: {
+ autocomplete: {
+ onSetSuggestions : function(e, data) {
+ var self = this,
+ val = self.val(),
+ suggestions = self._suggestions = data.result;
+ if(data.showHideDropdown !== false)
+ self.trigger(suggestions === null || suggestions.length === 0 && val.length === 0 ? "hideDropdown" : "showDropdown");
+ },
+ renderSuggestions: function(suggestions) {
+ var self = this,
+ val = self.val();
+ self.clearItems();
+ $.each(suggestions || [], function(index, item) {
+ self.addSuggestion(item);
+ });
+ var lowerCasesuggestions = $.map(suggestions, function(n,i){return n.toLowerCase();});
+ if(jQuery.inArray(val.toLowerCase(), lowerCasesuggestions) ==-1) {
+ self.addSuggestion("Create '" + val + "'");
+ }
+ },
+ },
+ tags: {
+ onEnterKeyPress: function(e) {
+ var self = this,
+ val = self.val(),
+ tag = self.itemManager().stringToItem(val);
+
+ if(self.isTagAllowed(tag)) {
+ tag = tag.replace(/Create\ '|\'|'/g,'');
+ self.addTags([ tag ]);
+ // refocus the textarea just in case it lost the focus
+ self.core().focusInput();
+ }
+ },
+ }
+ },
tagsItems: tags.split(","),
//Note: The following list of keyboard keys is added. All entries are default except {32 : 'whitespace!'}.
keys: {8: 'backspace', 9: 'tab', 13: 'enter!', 27: 'escape!', 37: 'left', 38: 'up!', 39: 'right',
- 40: 'down!', 46: 'delete', 108: 'numpadEnter', 32: 'whitespace!'},
+ 40: 'down!', 46: 'delete', 108: 'numpadEnter', 32: 'whitespace'},
ajax: {
url: '/forum/get_tags',
dataType: 'json',
cacheResults: true
}
});
- // Adds: create tags on space + blur
- $("input.load_tags").on('whitespaceKeyDown blur', function () {
- $(this).textext()[0].tags().addTags([ $(this).val() ]);
- $(this).val("");
- });
+
$("input.load_tags").on('isTagAllowed', function(e, data) {
if (_.indexOf($(this).textext()[0].tags()._formData, data.tag) != -1) {
data.result = false;
}
if ($('textarea.load_editor').length) {
- var editor = CKEDITOR.instances['content'];
- editor.on('instanceReady', CKEDITORLoadComplete);
+ $('textarea.load_editor').each(function () {
+ if (this['id']) {
+ CKEDITOR.replace(this['id']).on('instanceReady', CKEDITORLoadComplete);
+ }
+ });
}
}
});
<field name="arch" type="xml">
<tree string="Forums">
<field name="name"/>
+ <field name="allow_question"/>
+ <field name="allow_link"/>
+ <field name="allow_discussion"/>
</tree>
</field>
</record>
<field name="arch" type="xml">
<form string="Forum">
<sheet>
- <group>
+ <label for="name" class="oe_edit_only"/>
+ <h1>
<field name="name"/>
+ </h1>
- <group>
- <group string="Post Types">
+ </group>
+ <notebook>
++ <page string="Options">
++ <group string="Post Types">
+ <field name="allow_question"/>
+ <field name="allow_link"/>
+ <field name="allow_discussion"/>
+ <field name="default_allow"/>
- </group>
- <group string="Orders">
++ </group>
++ <group string="Orders">
+ <field name="default_order"/>
+ <label for="relevancy_option_first" string="Relevancy Computation"/>
+ <div>
+ (votes - 1) ** <field name="relevancy_option_first" class="oe_inline"/> / (days + 2) ** <field name="relevancy_option_second" class="oe_inline"/>
+ </div>
- </group>
- </group>
- <group>
- <group string="Earn Karma">
- <field name="karma_gen_question_new"/>
- <field name="karma_gen_question_upvote"/>
- <field name="karma_gen_question_downvote"/>
- <field name="karma_gen_answer_upvote"/>
- <field name="karma_gen_answer_downvote"/>
- <field name="karma_gen_answer_accept"/>
- <field name="karma_gen_answer_accepted"/>
- <field name="karma_gen_answer_flagged"/>
- </group>
- <group string="Karma Related Rights">
- <field name="karma_ask"/>
- <field name="karma_edit_own"/>
- <field name="karma_edit_all"/>
- <field name="karma_close_own"/>
- <field name="karma_close_all"/>
- <field name="karma_unlink_own"/>
- <field name="karma_unlink_all"/>
- <field name="karma_upvote"/>
- <field name="karma_downvote"/>
- <field name="karma_answer_accept_own"/>
- <field name="karma_answer_accept_all"/>
- <field name="karma_editor_link_files"/>
- <field name="karma_editor_clickable_link"/>
- <field name="karma_comment_own"/>
- <field name="karma_comment_all"/>
- <field name="karma_comment_convert_own"/>
- <field name="karma_comment_convert_all"/>
- <field name="karma_comment_unlink_own"/>
- <field name="karma_comment_unlink_all"/>
- </group>
- </group>
++ </group>
++ </page>
+ <page string='Karma Gains'>
+ <group>
+ <field name="karma_gen_question_new"/>
+ <field name="karma_gen_question_upvote"/>
+ <field name="karma_gen_question_downvote"/>
+ <field name="karma_gen_answer_upvote"/>
+ <field name="karma_gen_answer_downvote"/>
+ <field name="karma_gen_answer_accept"/>
+ <field name="karma_gen_answer_accepted"/>
++ <field name="karma_gen_answer_flagged"/>
+ </group>
+ </page>
- <page string='Karma Requirements'>
++ <page string='Karma Related Rights'>
+ <group>
+ <group>
+ <field name="karma_ask"/>
+ <field name="karma_upvote"/>
+ <field name="karma_downvote"/>
+ <field name="karma_edit_own"/>
+ <field name="karma_edit_all"/>
+ <field name="karma_close_own"/>
+ <field name="karma_close_all"/>
+ <field name="karma_unlink_own"/>
+ <field name="karma_unlink_all"/>
+ </group>
+ <group>
+ <field name="karma_answer_accept_own"/>
+ <field name="karma_answer_accept_all"/>
++ <field name="karma_editor_link_files"/>
++ <field name="karma_editor_clickable_link"/>
+ <field name="karma_comment_own"/>
+ <field name="karma_comment_all"/>
+ <field name="karma_comment_convert_own"/>
+ <field name="karma_comment_convert_all"/>
+ <field name="karma_comment_unlink_own"/>
+ <field name="karma_comment_unlink_all"/>
+ </group>
+ </group>
+ </page>
+ </notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="vote_count"/>
<field name="favourite_count"/>
<field name="child_count"/>
+ <field name="relevancy"/>
</group>
</group>
</sheet>
<openerp>
<data>
+<!-- Editor custom -->
+<template id="assets_editor" inherit_id="website.assets_editor" name="Forum Editor Assets" groups="base.group_user">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/website_forum/static/src/js/website.tour.forum.js"/>
+ <script type="text/javascript" src="/website_forum/static/src/js/website_forum.editor.js"/>
+ </xpath>
+</template>
+
+<!-- Front-end custom css / js + ckeditor lib and customization -->
+<template id="assets_frontend" inherit_id="website.assets_frontend" name="Forum Assets">
+ <xpath expr="." position="inside">
+ <link rel='stylesheet' href="/web/static/lib/jquery.textext/jquery.textext.css"/>
+ <link rel='stylesheet' href='/website_forum/static/src/css/website_forum.css'/>
+ <script type="text/javascript" src="/website_forum/static/src/js/website_forum.js"/>
+ <script type="text/javascript" src="/web/static/lib/jquery.textext/jquery.textext.js"/>
+ <script type="text/javascript">
+ var CKEDITOR_BASEPATH = '/web/static/lib/ckeditor/';
+ </script>
+ <script type="text/javascript" src="/web/static/lib/ckeditor/ckeditor.js"></script>
+ <script type="text/javascript">
+ CKEDITOR.config.toolbar = [['Bold','Italic','Underline','Strike'],['NumberedList','BulletedList', 'Blockquote']
+ ,['Outdent','Indent','Link','Unlink','Image'],] ;
+ </script>
+
+ </xpath>
+</template>
+
<!-- Layout add nav and footer -->
<template id="header_footer_custom" inherit_id="website.footer_default"
name="Footer Questions Link">
</form>
</template>
<!-- Page Index -->
<template id="header" name="Forum Index">
<t t-call="website.layout">
- <t t-set="head">
- <script type="text/javascript" src="/web/static/lib/ckeditor/ckeditor.js"/>
- <script type="text/javascript">
- CKEDITOR.config.toolbar = [['Bold','Italic','Underline','Strike'],['NumberedList','BulletedList', 'Blockquote']
- ,['Outdent','Indent','Link','Unlink','Image'],] ;
- </script>
- </t>
+ <div t-if="is_public_user and not no_introduction_message" class="alert alert-success alert-dismissable">
+ <div class="container">
+ <div t-field="forum.introduction_message"/>
+ <a class='btn btn-primary' t-attf-href="/web?redirect=#{ request.httprequest.url }">Register</a>
+ <button type="button" class="btn btn-link js_close_intro" data-dismiss="alert" aria-hidden="true">Hide Intro</button>
+ </div>
+ </div>
<div class="container mt16 website_forum">
<div class="navbar navbar-default">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#oe-help-navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
- <!-- <span class="icon-bar"></span> -->
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" t-attf-href="/forum/#{slug(forum)}">
</div>
<div class="collapse navbar-collapse" id="oe-help-navbar-collapse">
<ul class="nav navbar-nav">
- <li t-att-class="filters in ('all', 'unanswered','followed','question','tag') and 'active' or '' ">
- <a t-attf-href="/forum/#{ slug(forum) }">Questions</a>
+ <li t-att-class="sorting == 'relevancy desc' and 'active' or '' ">
+ <a t-attf-href="/forum/#{ slug(forum) }?{{ keep_query( 'search', 'post_type', 'filters', sorting='relevancy desc') }}">Trending</a>
+ </li>
+ <li t-att-class="sorting == 'create_date desc' and 'active' or '' ">
+ <a t-attf-href="/forum/#{ slug(forum) }?{{ keep_query( 'search', 'post_type', 'filters', sorting='create_date desc') }}">Newest</a>
</li>
<li t-att-class="searches.get('users') and 'active' or '' ">
<a t-attf-href="/forum/#{ slug(forum) }/users">People</a>
</li>
</ul>
<form class="navbar-form navbar-right" role="search" t-attf-action="/forum/#{ slug(forum) }" method="get">
- <div class="form-group">
- <input type="search" class="form-control"
- name="search" placeholder="Search a question..."
- t-att-value="search or ''"/>
- <button type="submit" class="btn btn-default">Search</button>
+ <div class="input-group">
+ <input type="search" class="form-control" name="search" t-att-value="search or ''"/>
+ <span class="input-group-btn">
+ <button type="submit" class="btn btn-default">Search</button>
+ </span>
</div>
</form>
</div>
<div t-field="notification.body"/>
<a t-attf-href="/forum/#{ slug(forum) }/user/#{ user.id }#badges" class="fa fa-arrow-right">View Your Badges</a>
</div>
+ <div t-if="not validation_email_sent and not is_public_user and user.karma == 0" class="alert alert-danger alert-dismissable">
+ <button type="button" class="close validation_email_close" data-dismiss="alert" aria-hidden="true">&times;</button>
+ <div>
+ <p>
+ It appears your email has not been verified.
+ <a class="send_validation_email" href="#" t-att-forum-id="forum.id">Click here to send a verification email allowing you to participate to the forum.</a>
+ </p>
+ </div>
+ </div>
+ <div t-if="validation_email_done" class="alert alert-success alert-dismissable">
+ <button type="button" class="close validated_email_close" data-dismiss="alert" aria-hidden="true">&times;</button>
+ <div>
+ <p>Congratulations! Your email has just been validated. You may now participate to our forums.</p>
+ </div>
+ </div>
<t t-raw="0"/>
</div>
<div class="col-sm-3" id="right-column">
- <div t-if="not header.get('ask_hide')" t-attf-class="btn-group btn-block mb16 #{user.karma >= forum.karma_ask and '' or 'karma_required'}" t-attf-data-karma="#{forum.karma_ask}">
- <a t-if="not header.get('ask_hide')"
- t-attf-class="btn btn-primary btn-lg btn-block mb16 #{(user.karma < forum.karma_ask) and 'karma_required' or ''}"
- t-attf-href="/forum/#{slug(forum)}/ask"
- t-attf-data-karma="#{forum.karma_ask}">Ask a Question</a>
++ <div t-if="not header.get('ask_hide')" t-attf-class="btn-group btn-block mb16 #{(user.karma < forum.karma_ask) and 'karma_required' or ''}" t-attf-data-karma="#{forum.karma_ask}">
+ <a type="button" class="btn btn-primary btn-lg col-sm-10" t-attf-href="/forum/#{slug(forum)}/#{forum.default_allow}">
+ <t t-if="forum.default_allow == 'ask_question'">Ask a Question</t>
+ <t t-if="forum.default_allow == 'post_link'">Submit a Post</t>
+ <t t-if="forum.default_allow == 'post_discussion'">New Discussion</t>
+ </a>
+ <button type="button" class="btn btn-primary btn-lg col-sm-2 dropdown-toggle" data-toggle="dropdown">
+ <span class="caret"></span>
+ <span class="sr-only">Select Post</span>
+ </button>
+ <ul class="dropdown-menu" role="menu">
+ <li t-if="forum.allow_link"><a t-attf-href="/forum/#{slug(forum)}/post_link">Submit a Post</a></li>
+ <li t-if="forum.allow_question"><a t-attf-href="/forum/#{slug(forum)}/ask_question">Ask a Question</a></li>
+ <li t-if="forum.allow_discussion"><a t-attf-href="/forum/#{slug(forum)}/post_discussion">Launch a Discussion</a></li>
+ </ul>
+ </div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Keep Informed</h3>
</div>
<div class="panel panel-default" id="about_forum">
<div class="panel-heading">
- <h3 class="panel-title">About This Forum</h3>
+ <h3 class="panel-title">About This Community</h3>
</div>
<div class="panel-body">
<t t-raw="forum.description"/>
<template id="display_post">
<div class="question row">
<div class="col-md-2 hidden-xs text-center">
- <div t-attf-class="box #{question.is_correct and 'oe_green' or 'oe_grey'} #{(question.child_count == 0) and 'text-muted' or ''}">
- <span t-esc="question.child_count"/>
- <div t-if="question.child_count>1" class="subtitle">Answers</div>
- <div t-if="question.child_count<=1" class="subtitle">Answer</div>
- </div>
+ <t t-call="website_forum.vote">
+ <t t-set="post" t-value="question"/>
+ </t>
</div>
<div class="col-md-10 clearfix">
<div class="question-name">
- <a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }" t-field="question.name"/>
- <span t-if="not question.active"><b> [Deleted]</b></span>
- <span t-if="question.state == 'close'"><b> [Closed]</b></span>
+ <t t-if="question.type == 'link'">
+ <a t-att-href="question.content_link" t-raw="question.name"/>
+ </t>
+ <t t-if="question.type in ('question', 'discussion')">
+ <a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }" t-field="question.name"/>
+ </t>
+ <span t-if="not question.active"><b> [Deleted]</b></span>
+ <span t-if="question.state == 'close'"><b> [Closed]</b></span>
</div>
<t t-foreach="question.tag_ids" t-as="question_tag">
<a t-attf-href="/forum/#{ slug(forum) }/tag/#{slug(question_tag)}/questions">
- <span t-attf-class="pull-right badge #{tag and tag.name == question_tag.name and 'badge-active' ''}" t-field="question_tag.name"
+ <span t-attf-class="pull-right label #{tag and tag.name == question_tag.name and 'label-primary' or 'label-default'}" t-field="question_tag.name"
style="margin-right: 4px;"/>
</a>
</t>
- <div class="text-muted">
- by <a t-attf-href="/forum/#{ slug(forum) }/user/#{ question.create_uid.id }"
- t-field="question.create_uid" t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}'
- style="display: inline-block;"/>
- on <span t-field="question.write_date" t-field-options='{"format":"short"}'/>
- <span class="visible-xs">
- <b t-esc="question.child_count or 0"/>
- <t t-if="question.child_count>1">answers</t>
- <t t-if="question.child_count==1">answers</t>
- </span>
- with <b t-field="question.views"/> views
- <span t-if="question.vote_count>0"> and
- <b t-esc="question.vote_count or 0"/>
- <t t-if="question.vote_count>1">votes</t>
- <t t-if="question.vote_count==1">vote</t>
- </span>
- </div>
+ <small class="text-muted">
+ By <span t-field="question.create_uid" t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}' style="display: inline-block;"/>
+ • <span t-field="question.write_date" t-field-options='{"format":"short"}'/>
+ • <a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">
+ <t t-esc="question.child_count"/>
+ <t t-if="question.child_count>1">comments</t>
+ <t t-if="question.child_count<=1">comment</t>
+ </a>
+ </small>
</div>
</div>
</template>
<!-- Specific Forum Layout -->
<template id="forum_index" name="Forum">
<t t-call="website_forum.header">
- <h1 class="page-header mt0">
- <t t-esc="question_count"/> <span>Questions</span>
+ <h2 class="page-header mt0">
+ <t t-esc="question_count"/>
+ <span t-if="post_type == 'all'">Posts</span>
+ <span t-if="post_type == 'question'">Questions</span>
+ <span t-if="post_type == 'link'">Posts</span>
+ <span t-if="post_type == 'discussion'">Discussions</span>
<t t-esc="search"/>
<small class="dropdown" t-if="filters in ('all', 'unanswered','followed', 'tag')">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<t t-if="filters == 'unanswered'">Unanswered</t>
<t t-if="filters == 'followed'">Followed</t>
<t t-if="tag"><span t-field="tag.name"/></t>
- <t t-if="sorting == 'date'"> by activity date</t>
- <t t-if="sorting == 'creation'"> by creation date</t>
- <t t-if="sorting == 'answered'"> by most answered</t>
- <t t-if="sorting == 'vote'"> by most voted</t>
+ <t t-if="sorting == 'relevancy desc'"> by relevancy</t>
+ <t t-if="sorting == 'write_date desc'"> by activity date</t>
+ <t t-if="sorting == 'create_date desc'"> by newest</t>
+ <t t-if="sorting == 'child_count desc'"> by most answered</t>
+ <t t-if="sorting == 'vote_count desc'"> by most voted</t>
<b class="caret"/>
</a>
<ul class="dropdown-menu">
<a href=""><t t-esc="tag.name"/></a>
</li>
<li class="dropdown-header">Sort by</li>
- <li t-att-class="sorting == 'date' and 'active' or '' ">
- <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='date')">Last activity date</a>
+ <li t-att-class="sorting == 'relevancy desc' and 'active' or '' ">
+ <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='relevancy desc')">Relevancy</a>
+ </li>
+ <li t-att-class="sorting == 'write_date desc' and 'active' or '' ">
+ <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='write_date desc')">Last activity date</a>
</li>
- <li t-att-class="sorting == 'creation' and 'active' or '' ">
- <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='creation')">Newest</a>
+ <li t-att-class="sorting == 'create_date desc' and 'active' or '' ">
+ <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='create_date desc')">Newest</a>
</li>
- <li t-att-class="sorting == 'answered' and 'active' or '' ">
- <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='answered')">Most answered</a>
+ <li t-att-class="sorting == 'child_count desc' and 'active' or '' ">
+ <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='child_count desc')">Most answered</a>
</li>
- <li t-att-class="sorting == 'vote' and 'active' or '' ">
- <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='vote')">Most voted</a>
+ <li t-att-class="sorting == 'vote_count desc' and 'active' or '' ">
+ <a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', sorting='vote_count desc')">Most voted</a>
</li>
</ul>
</small>
- </h1>
+ </h2>
<div t-foreach="question_ids" t-as="question" class="mb16">
<t t-call="website_forum.display_post"/>
</div>
</t>
</template>
+<!-- Edition: Post Article -->
+<template id="post_link">
+ <t t-call="website_forum.header">
+ <h1 class="mt0">Submit a Link</h1>
+ <p class="mb32">
+ Share an awesome link. Your post will appear in the 'Newest' top-menu.
+ If the community vote on your post, it will get traction by being promoted
+ in the homepage.
+ </p><p>
+ We keep a high level of quality in showcased posts, only 20% of the submited
+ posts will be featured.
+ </p>
+ <form t-attf-action="/forum/#{ slug(forum) }/link/new" method="post" role="form" class="tag_text form-horizontal">
+ <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
+ <div class="form-group">
+ <label class="col-sm-2 control-label" for="content_link">URL to Share</label>
+ <div class="col-sm-8">
+ <input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
+ class="form-control mb16 link_url" placeholder="e.g. https://www.odoo.com"/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2 control-label" for="post_name">Post Title</label>
+ <div class="col-sm-8">
+ <input type="text" name="content" readonly="True" required="True" t-attf-value="#{content}"
+ class="form-control"/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2 control-label" for="post_tags">Tags</label>
+ <div class="col-sm-8">
+ <input type="text" name="post_tags" readonly="True" class="form-control load_tags"/>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-8">
+ <button class="btn btn-primary" disabled="True" id="btn_post_your_article">Post Your Article</button>
+ </div>
+ </div>
+ </form>
+ </t>
+</template>
+
+<!-- Edition: Post your Discussion Topic -->
+<template id="post_discussion">
+ <t t-call="website_forum.header">
+ <h1 class="mt0">Post Your Discussion Topic</h1>
+ <p>
+ <b>Share</b> Start Something Awesome
+ </p>
+ <form t-attf-action="/forum/#{ slug(forum) }/discussion/new" method="post" role="form" class="tag_text">
+ <input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
+ class="form-control mb16" placeholder="Your Discussion Title..."/>
+ <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
+ <textarea name="content" id="content" required="True" class="form-control load_editor">
+ <t t-esc="question_content"/>
+ </textarea>
+ <br/>
+ <input type="text" name="post_tags" placeholder="Tags" class="form-control load_tags"/>
+ <br/>
+ <button class="btn btn-primary" id="btn_ask_your_question">Post Your Topic</button>
+ </form>
+ </t>
+</template>
+
<!-- Edition: ask your question -->
<template id="ask_question">
<t t-call="website_forum.header">
- <h1 class="mt0">Ask your Question</h1>
+ <t t-set="head">
+ <script type="text/javascript">
+ $(function () {
+ $("[data-toggle='popover']").popover();
+ });
+ </script>
+ </t>
+ <h1 class="mt0">Ask Your Question</h1>
+ <p>
+ To improve your chance getting an answer:
+ </p>
<ul>
- <li> please, try to make your question interesting to others </li>
- <li> provide enough details and, if possible, give an example </li>
- <li> be clear and concise, avoid unnecessary introductions (Hi, ... Thanks...) </li>
+ <li>Set a clear, explicit and concise question title
+ (check
+ <a href="#" data-placement="top" data-toggle="popover" data-content="Inventory Date Problem, Task remaining hours, Can you help solve solve my tax computation problem in Canada?" title="Click to get bad question samples">bad examples</a>
+ and
+ <a href="#" data-placement="bottom" data-toggle="popover" data-content="How to create a physical inventory at an anterior date?, How is the 'remaining hours' field computed on tasks?, How to configure TPS and TVQ's canadian taxes?" title="Click to get good question titles">good examples</a>
+ ),
+ </li>
+ <li>Avoid unnecessary introductions (Hi,... Please... Thanks...),</li>
+ <li>Provide enough details and, if possible, give an example.</li>
</ul>
<form t-attf-action="/forum/#{ slug(forum) }/question/new" method="post" role="form" class="tag_text">
- <input type="text" name="question_name" required="True" t-attf-value="#{question_name}"
- class="form-control" placeholder="Enter your Question"/>
- <h5 class="mt20">Please enter a descriptive question (should finish with a '?')</h5>
+ <input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
+ class="form-control mb16" placeholder="Your Question Title..."/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
- <textarea name="content" required="True" class="form-control load_editor">
+ <textarea name="content" required="True" id="content" class="form-control load_editor">
<t t-esc="question_content"/>
</textarea>
<br/>
- <input type="text" name="question_tags" placeholder="Tags" class="form-control load_tags"/>
+ <input type="text" name="post_tags" placeholder="Tags" class="form-control load_tags"/>
<br/>
- <button t-attf-class="btn btn-primary #{(user.karma <= forum.karma_ask) and 'karma_required' or ''}"
+ <button t-attf-class="btn btn-primary #{(user.karma < forum.karma_ask) and 'karma_required' or ''}"
id="btn_ask_your_question" t-att-data-karma="forum.karma_ask">Post Your Question</button>
</form>
- <script type="text/javascript">
- CKEDITOR.replace("content");
- </script>
</t>
</template>
<!-- Edition: edit a post -->
<template id="edit_post">
<t t-call="website_forum.header">
- <h3 t-if="not is_answer">Edit question</h3>
- <h3 t-if="is_answer">Edit answer</h3>
+ <h3 t-if="not is_answer">Edit <span t-field="post.type"/></h3>
+ <h3 t-if="is_answer">Edit reply</h3>
<form t-attf-action="/forum/#{slug(forum)}/post/#{slug(post)}/save" method="post" role="form" class="tag_text">
<div t-if="not is_answer">
- <input type="text" name="question_name" id="question_name" required="True"
- t-attf-value="#{post.name}" class="form-control" placeholder="Edit your Question"/>
- <h5 class="mt20">Please enter a descriptive question (should finish by a '?')</h5>
+ <input type="text" name="post_name" required="True"
+ t-attf-value="#{post.name}" class="form-control mb8" placeholder="Edit your Post"/>
+ <h5 t-if="post.type == 'question'" class="mt20">Please enter a descriptive question (should finish by a '?')</h5>
</div>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
- <textarea name="content" required="True" class="form-control load_editor">
+ <textarea name="content" id="content" required="True" class="form-control load_editor">
<t t-esc="post.content"/>
</textarea>
<div t-if="not is_answer">
<br/>
- <input type="text" name="question_tag" class="form-control col-md-9 load_tags" placeholder="Tags" t-attf-value="#{tags}"/>
+ <input type="text" name="post_tag" class="form-control col-md-9 load_tags" placeholder="Tags" t-attf-value="#{tags}"/>
<br/>
</div>
<button class="btn btn-primary btn-lg">Save</button>
</form>
</t>
</template>
-<!-- Moderation: close a question -->
-<template id="close_question">
+<!-- Moderation: close a post -->
+<template id="close_post">
<t t-call="website_forum.header">
- <h1 class="mt0">Close question</h1>
+ <h1 class="mt0">Close Post</h1>
<p class="text-muted">
- If you close this question, it will be hidden for most users. Only
- users having a high karma can see closed questions to moderate
+ If you close this post, it will be hidden for most users. Only
+ users having a high karma can see closed posts to moderate
them.
</p>
<form t-attf-action="/forum/#{ slug(forum) }/question/#{slug(question)}/close" method="post" role="form" class="form-horizontal mt32 mb64">
<input name="post_id" t-att-value="question.id" type="hidden"/>
<div class="form-group">
- <label class="col-md-3 control-label" for="reason">Question:</label>
+ <label class="col-md-3 control-label" for="reason">Post:</label>
<div class="col-md-8 mt8">
<span t-field="question.name"/>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-3 col-md-8">
- <button class="btn btn-primary">Close question</button>
+ <button class="btn btn-primary">Close post</button>
<span class="text-muted">or</span>
- <a class="btn btn-link" t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">back to question</a>
+ <a class="btn btn-link" t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">back to post</a>
</div>
</div>
</form>
</t>
</template>
+<!-- Edition: post a reply -->
+<template id="post_reply">
+ <div class="css_editable_mode_hidden">
+ <form t-attf-id="reply#{ object._name.replace('.','') + '-' + str(object.id) }" class="collapse oe_comment_grey"
+ t-attf-action="/forum/#{ slug(forum) }/#{slug(object)}/reply" method="post" role="form">
+ <h3 class="mt10">Your Reply</h3>
+ <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
+ <textarea name="content" t-attf-id="content-#{str(object.id)}" class="form-control load_editor" required="True"/>
+ <button class="btn btn-primary" id="btn_ask_your_question">Post Your Reply</button>
+ </form>
+ </div>
+</template>
+
<!-- Edition: post an answer -->
<template id="post_answer">
- <h3 class="mt10">Your answer</h3>
- <p>
+ <h3 class="mt10">Your Reply</h3>
+ <p t-if="question.type == 'question'">
<b>Please try to give a substantial answer.</b> If you wanted to comment on the question or answer, just
<b>use the commenting tool.</b> Please remember that you can always <b>revise your answers</b>
- no need to answer the same question twice. Also, please <b>don't forget to vote</b>
- it really helps to select the best questions and answers!
</p>
- <form t-attf-action="/forum/#{ slug(forum) }/post/#{slug(question)}/new" method="post" role="form">
+ <form t-attf-action="/forum/#{ slug(forum) }/#{slug(question)}/reply" method="post" role="form">
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
- <textarea name="content" class="form-control load_editor" required="True"/>
+ <textarea name="content" t-attf-id="content-#{str(question.id)}" class="form-control load_editor" required="True"/>
<button t-attf-class="btn btn-primary mt16 #{not question.can_answer and 'karma_required' or ''}"
- id="btn_ask_your_question" t-att-data-karma="question.karma_answer">Post Your Answer</button>
+ id="btn_ask_your_question" t-att-data-karma="question.karma_answer">Post Your Reply</button>
</form>
- <script type="text/javascript">
- CKEDITOR.replace("content");
- </script>
</template>
<template id="vote">
<!-- Specific Post Layout -->
<template id="post_description_full" name="Question Navigation">
<t t-call="website_forum.header">
- <div t-attf-class="row question #{not question.active and 'alert alert-danger' or ''}">
+ <div t-attf-class="row question">
<div class="col-md-2 hidden-xs text-center">
<t t-call="website_forum.vote">
<t t-set="post" t-value="question"/>
t-attf-class="favourite_question no-decoration fa fa-2x fa-star #{question.user_favourite and 'forum_favourite_question' or ''}"/>
</div>
</div>
- <div class="col-md-10">
+ <div t-attf-class="col-md-10 #{not question.active and 'alert alert-danger' or ''}">
<h1 class="mt0">
- <a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }" t-field="question.name"/>
+ <t t-if="question.type == 'link'">
+ <a t-att-href="question.content_link" t-raw="question.name"/>
+ </t>
+ <t t-if="question.type in ('question', 'discussion')">
+ <a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }" t-field="question.name"/>
+ </t>
<span t-if="not question.active"><b> [Deleted]</b></span>
<span t-if="question.state == 'close'"><b> [Closed]</b></span>
</h1>
<div class="alert alert-info text-center" t-if="question.state == 'close'">
<p class="mt16">
- <b>The question has been closed<t t-if="question.closed_reason_id"> for reason: <i t-esc="question.closed_reason_id.name"/></t></b>
+ <b>The <i t-field="question.type"/> has been closed<t t-if="question.closed_reason_id"> for reason: <i t-esc="question.closed_reason_id.name"/></t></b>
</p>
<t t-if="question.closed_uid">
<b>by <a t-attf-href="/forum/#{ slug(forum) }/user/#{ question.closed_uid.id }"
</t>
</div>
</div>
- <t t-raw="question.content"/>
+ <div t-if="question.type != 'link'"><t t-raw="question.content"/></div>
<div class="mt16 clearfix">
<div class="pull-right">
<div class="text-right">
<t t-foreach="question.tag_ids" t-as="tag">
- <a t-attf-href="/forum/#{ slug(forum) }/tag/#{ tag.id }/questions" class="badge" t-field="tag.name"/>
+ <a t-attf-href="/forum/#{ slug(forum) }/tag/#{ tag.id }/questions" class="label label-default" t-field="tag.name"/>
</t>
</div>
<ul class="list-inline" id="options">
- <li>
+ <li t-if="question.type == 'question'">
<a style="cursor: pointer" t-att-data-toggle="question.can_comment and 'collapse' or ''"
t-attf-class="fa fa-comment-o #{not question.can_comment and 'karma_required text-muted' or ''}"
t-attf-data-karma="#{not question.can_comment and question.karma_comment or 0}"
</div>
</div>
<hr/>
+ <div t-foreach="question.child_ids" t-as="post_answer" class="mt16 mb32">
+ <t t-call="website_forum.post_answers">
+ <t t-set="answer" t-value="post_answer"/>
+ </t>
+ </div>
+ <div t-if="question.type != 'question' or question.type == 'question' and not question.uid_has_answered and question.state != 'close' and question.active != False">
+ <t t-call="website_forum.post_answer"/>
+ </div>
+ <div t-if="question.type == 'question' and question.uid_has_answered" class="mb16">
+ <a class="btn btn-primary" t-attf-href="/forum/#{slug(forum)}/question/#{slug(question)}/edit_answer">Edit Your Previous Answer</a>
+ <span class="text-muted">(only one answer per question is allowed)</span>
+ </div>
+ </t>
+</template>
- <div t-foreach="question.child_ids" t-as="answer" class="mt16 mb32">
- <a t-attf-id="answer-#{str(answer.id)}"/>
- <div t-attf-class="forum_answer row" t-attf-id="answer_#{answer.id}" >
- <div class="col-md-2 hidden-xs text-center">
+<template id="post_answers">
+ <a t-attf-id="answer-#{str(answer.id)}"/>
+ <div t-attf-class="forum_answer row" t-attf-id="answer_#{answer.id}" >
+ <div class="col-md-2 hidden-xs text-center">
+ <t t-call="website_forum.vote">
+ <t t-set="post" t-value="answer"/>
+ </t>
+ <div t-if="question.type == 'question'" class="text-muted mt8">
+ <a t-attf-class="accept_answer fa fa-2x fa-check-circle no-decoration #{answer.is_correct and 'oe_answer_true' or 'oe_answer_false'} #{not answer.can_accept and 'karma_required' or ''}"
+ t-attf-data-karma="#{answer.karma_accept}"
+ t-attf-data-href="/forum/#{slug(question.forum_id)}/post/#{slug(answer)}/toggle_correct"/>
+ </div>
+ </div>
+ <div class="col-md-10 clearfix">
+ <t t-raw="answer.content"/>
+ <div class="mt16">
+ <ul class="list-inline pull-right">
+ <li t-if="question.type == 'question'">
+ <a t-attf-class="fa fa-comment-o #{not answer.can_comment and 'karma_required text-muted' or ''}"
+ t-attf-data-karma="#{not answer.can_comment and answer.karma_comment or 0}"
+ style="cursor: pointer" t-att-data-toggle="answer.can_comment and 'collapse' or ''"
+ t-attf-data-target="#comment#{ answer._name.replace('.','') + '-' + str(answer.id) }"> Comment
+ </a>
+ </li>
+ <li t-if="question.type != 'question' and not answer.parent_id or answer.parent_id and not answer.parent_id.parent_id">
+ <a t-attf-class="fa fa-comment-o #{not answer.can_comment and 'karma_required text-muted' or ''}"
+ t-attf-data-karma="#{not answer.can_comment and answer.karma_comment or 0}"
+ style="cursor: pointer" data-toggle="collapse"
+ t-attf-data-target="#reply#{ answer._name.replace('.','') + '-' + str(answer.id) }"> Reply
+ </a>
+ </li>
+ <li>
+ <t t-call="website_forum.link_button">
+ <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/edit'"/>
+ <t t-set="label" t-value="'Edit'"/>
+ <t t-set="classes" t-value="'fa fa-edit'"/>
+ <t t-set="karma" t-value="not answer.can_edit and answer.karma_edit or 0"/>
+ </t>
+ </li>
+ <li>
+ <t t-call="website_forum.link_button">
+ <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/delete'"/>
+ <t t-set="label" t-value="'Delete'"/>
+ <t t-set="classes" t-value="'fa-trash-o'"/>
+ <t t-set="karma" t-value="not answer.can_unlink and answer.karma_unlink or 0"/>
+ </t>
+ </li>
+ <li t-if="question.type == 'question'">
+ <t t-call="website_forum.link_button">
+ <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/convert_to_comment'"/>
+ <t t-set="label" t-value="'Convert as a comment'"/>
+ <t t-set="classes" t-value="'fa-magic'"/>
+ <t t-set="karma" t-value="not answer.can_comment_convert and answer.karma_comment_convert or 0"/>
+ </t>
+ </li>
+ </ul>
+ <img class="pull-left img img-circle img-avatar" t-attf-src="/forum/user/#{answer.create_uid.id}/avatar"/>
+ <div>
+ <a t-attf-href="/forum/#{ slug(forum) }/user/#{ answer.create_uid.id }"
+ t-field="answer.create_uid"
+ t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}'
+ style="display: inline-block;"/>
+ <div t-field="answer.create_uid" t-field-options='{"widget": "contact", "badges": true, "fields": ["karma"]}'/>
+ <span class="text-muted">Answered on <span t-field="answer.create_date" t-field-options='{"format":"short"}'/></span>
+ </div>
+ <div class="visible-xs text-center">
<t t-call="website_forum.vote">
<t t-set="post" t-value="answer"/>
</t>
t-attf-data-href="/forum/#{slug(question.forum_id)}/post/#{slug(answer)}/toggle_correct"/>
</div>
</div>
- <div class="col-md-10 clearfix">
- <t t-raw="answer.content"/>
- <div class="mt16">
- <ul class="list-inline pull-right">
- <li>
- <a t-attf-class="fa fa-comment-o #{not answer.can_comment and 'karma_required text-muted' or ''}"
- t-attf-data-karma="#{not answer.can_comment and answer.karma_comment or 0}"
- style="cursor: pointer" t-att-data-toggle="answer.can_comment and 'collapse' or ''"
- t-attf-data-target="#comment#{ answer._name.replace('.','') + '-' + str(answer.id) }"> Comment
- </a>
- </li>
- <li>
- <t t-call="website_forum.link_button">
- <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/edit'"/>
- <t t-set="label" t-value="'Edit'"/>
- <t t-set="classes" t-value="'fa fa-edit'"/>
- <t t-set="karma" t-value="not answer.can_edit and answer.karma_edit or 0"/>
- </t>
- </li>
- <li>
- <t t-call="website_forum.link_button">
- <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/delete'"/>
- <t t-set="label" t-value="'Delete'"/>
- <t t-set="classes" t-value="'fa-trash-o'"/>
- <t t-set="karma" t-value="not answer.can_unlink and answer.karma_unlink or 0"/>
- </t>
- </li>
- <li>
- <t t-call="website_forum.link_button">
- <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/convert_to_comment'"/>
- <t t-set="label" t-value="'Convert as a comment'"/>
- <t t-set="classes" t-value="'fa-magic'"/>
- <t t-set="karma" t-value="not answer.can_comment_convert and answer.karma_comment_convert or 0"/>
- </t>
- </li>
- </ul>
- <img class="pull-left img img-circle img-avatar" t-attf-src="/forum/user/#{answer.create_uid.id}/avatar"/>
- <div>
- <a t-attf-href="/forum/#{ slug(forum) }/user/#{ answer.create_uid.id }"
- t-field="answer.create_uid"
- t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}'
- style="display: inline-block;"/>
- <div t-field="answer.create_uid" t-field-options='{"widget": "contact", "badges": true, "fields": ["karma"]}'/>
- <span class="text-muted">Answered on <span t-field="answer.create_date" t-field-options='{"format":"short"}'/></span>
- </div>
- <div class="visible-xs text-center">
- <t t-call="website_forum.vote">
- <t t-set="post" t-value="answer"/>
- </t>
- <div class="text-muted mt8">
- <a t-attf-class="accept_answer fa fa-2x fa-check-circle no-decoration #{answer.is_correct and 'oe_answer_true' or 'oe_answer_false'} #{not answer.can_accept and 'karma_required' or ''}"
- t-attf-data-karma="#{answer.karma_accept}"
- t-attf-data-href="/forum/#{slug(question.forum_id)}/post/#{slug(answer)}/toggle_correct"/>
- </div>
- </div>
- </div>
- <t t-call="website_forum.post_comment">
- <t t-set="object" t-value="answer"/>
- </t>
- </div>
+ </div>
+ <t t-if="answer.type == 'question'" t-call="website_forum.post_comment">
+ <t t-set="object" t-value="answer"/>
+ </t>
+ <div t-if="answer.type != 'question' and question.state != 'close' and question.active != False">
+ <t t-call="website_forum.post_reply">
+ <t t-set="object" t-value="answer"/>
+ </t>
+ </div>
+ <div t-foreach="answer.child_ids" t-as="child_answer" class="mt16 mb16">
+ <t t-call="website_forum.post_answers">
+ <t t-set="answer" t-value="child_answer"/>
+ </t>
</div>
</div>
- <div t-if="not question.uid_has_answered">
- <t t-call="website_forum.post_answer"/>
- </div>
- <div t-if="question.uid_has_answered" class="mb16">
- <a class="btn btn-primary" t-attf-href="/forum/#{slug(forum)}/question/#{slug(question)}/edit_answer">Edit Your Previous Answer</a>
- <span class="text-muted">(only one answer per question is allowed)</span>
- </div>
- </t>
+ </div>
</template>
<!-- Utility template: Post a Comment -->
<span t-field="message.body"/>
<t t-set="required_karma" t-value="message.author_id.id == user.partner_id.id and object.forum_id.karma_comment_convert_own or object.forum_id.karma_comment_convert_all"/>
- <t t-call="website_forum.link_button">
- <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/convert_to_answer'"/>
- <t t-set="label" t-value="'Convert as an answer'"/>
- <t t-set="karma" t-value="user.karma<required_karma and required_karma or 0"/>
- <t t-set="classes" t-value="'fa-magic pull-right'"/>
+ <t t-if="(object.parent_id and object.parent_id.state != 'close' and object.parent_id.active != False) or (not object.parent_id and object.state != 'close' and object.active != False)">
+ <t t-set="allow_post_comment" t-value="True" />
+ </t>
+ <t t-if="allow_post_comment">
+ <t t-call="website_forum.link_button" >
+ <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/convert_to_answer'"/>
+ <t t-set="label" t-value="'Convert as an answer'"/>
+ <t t-set="karma" t-value="user.karma<required_karma and required_karma or 0"/>
+ <t t-set="classes" t-value="'fa-magic pull-right'"/>
+ </t>
</t>
<a t-attf-href="/forum/#{slug(forum)}/partner/#{message.author_id.id}"
t-field="message.author_id" t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}'
</p>
<div class="row">
<div class="col-sm-3 mt16" t-foreach="tags" t-as="tag">
- <a t-attf-href="/forum/#{ slug(forum) }/tag/#{ slug(tag) }/questions?{{ keep_query( filters='tag') }}" class="badge">
+ <a t-attf-href="/forum/#{ slug(forum) }/tag/#{ slug(tag) }/questions?{{ keep_query( filters='tag') }}" class="label label-default">
<span t-field="tag.name"/>
</a>
<span>
<openerp>
<data>
+<template id="assets_editor" inherit_id="website.assets_editor" name="Email Designer">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/website_mail/static/src/js/website_email_designer.js"></script>
+ </xpath>
+</template>
+
<!-- Template Choice page -->
<template id="email_designer" name="Email Designer">
<t t-call="website.layout">
</div>
<div t-foreach="templates" t-as="template" class="col-md-3 col-sm-4 text-center">
<div class="email_preview_border">
- <div t-esc="html_sanitize(template.body_html)" class="email_preview js_content"/>
+ <div t-raw="html_sanitize(template.body_html)" class="email_preview js_content"/>
</div>
<h4 t-field="template.name"/>
<button class="btn btn-primary js_template_set">Select</button>
</div>
</div>
<div id="email_designer" class="mb32" t-att-style="mode != 'email_designer' and 'display: none' or ''">
- <a class="mt16 btn btn-primary pull-right"
+ <a class="mt16 btn btn-primary pull-right" id="save_and_continue"
t-attf-href="/web#return_label=Website&model=#{model}&id=#{res_id}&view_type=form">
Save and Continue
</a>
for p in products:
x = min(max(p.website_size_x, 1), PPR)
y = min(max(p.website_size_y, 1), PPR)
- if index>PPG:
+ if index>=PPG:
x = y = 1
pos = minpos
while not self._check_place(pos%PPR, pos/PPR, x, y):
pos += 1
-
- if index>PPG and (pos/PPR)>maxy:
+ # if 21st products (index 20) and the last line is full (PPR products in it), break
+ # (pos + 1.0) / PPR is the line where the product would be inserted
+ # maxy is the number of existing lines
+ # + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block
+ # and to force python to not round the division operation
+ if index >= PPG and ((pos + 1.0) / PPR) > maxy:
break
if x==1 and y==1: # simple heuristic for CPU optimization
else:
pricelist = pool.get('product.pricelist').browse(cr, uid, context['pricelist'], context)
- product_obj = pool.get('product.template')
-
url = "/shop"
- product_count = product_obj.search_count(cr, uid, domain, context=context)
if search:
post["search"] = search
if category:
url = "/shop/category/%s" % slug(category)
- pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
- product_ids = product_obj.search(cr, uid, domain, limit=PPG+10, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
- products = product_obj.browse(cr, uid, product_ids, context=context)
style_obj = pool['product.style']
style_ids = style_obj.search(cr, uid, [], context=context)
categories = category_obj.browse(cr, uid, category_ids, context=context)
categs = filter(lambda x: not x.parent_id, categories)
+ domain += [('public_categ_ids', 'in', category_ids)]
+ product_obj = pool.get('product.template')
+
+ product_count = product_obj.search_count(cr, uid, domain, context=context)
+ pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
+ product_ids = product_obj.search(cr, uid, domain, limit=PPG+10, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
+ products = product_obj.browse(cr, uid, product_ids, context=context)
+
attributes_obj = request.registry['product.attribute']
attributes_ids = attributes_obj.search(cr, uid, [], context=context)
attributes = attributes_obj.browse(cr, uid, attributes_ids, context=context)
'sale_order_id': order.id,
}, context=context)
request.session['sale_transaction_id'] = tx_id
+ tx = transaction_obj.browse(cr, SUPERUSER_ID, tx_id, context=context)
# update quotation
request.registry['sale.order'].write(
'payment_tx_id': request.session['sale_transaction_id']
}, context=context)
+ # confirm the quotation
+ if tx.acquirer_id.auto_confirm == 'at_pay_now':
+ request.registry['sale.order'].action_button_confirm(cr, SUPERUSER_ID, [order.id], context=request.context)
+
return tx_id
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
t-attf-class="oe_product oe_grid oe-height-#{td_product['y']*2} #{ td_product['class'] }">
<div class="oe_product_cart" t-att-data-publish="product.website_published and 'on' or 'off'">
-
- <div class="css_options" t-ignore="true" groups="base.group_website_publisher">
- <div t-attf-class="dropdown js_options" t-att-data-id="product.id">
- <button class="btn btn-default" t-att-id="'dopprod-%s' % product.id" role="button" data-toggle="dropdown">Options <span class="caret"></span></button>
- <ul class="dropdown-menu" role="menu" t-att-aria-labelledby="'dopprod-%s' % product.id">
- <li class='dropdown-submenu'>
- <a tabindex="-1" href="#">Size</a>
- <ul class="dropdown-menu" name="size">
- <li><a href="#">
- <table>
- <tr>
- <td class="selected"></td>
- <td t-att-class="product.website_size_x > 1 and 'selected'"></td>
- <td t-att-class="product.website_size_x > 2 and 'selected'"></td>
- <td t-att-class="product.website_size_x > 3 and 'selected'"></td>
- </tr>
- <tr>
- <td t-att-class="product.website_size_y > 1 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 1 and product.website_size_x > 1 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 1 and product.website_size_x > 2 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 1 and product.website_size_x > 3 and 'selected'"></td>
- </tr>
- <tr>
- <td t-att-class="product.website_size_y > 2 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 2 and product.website_size_x > 1 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 2 and product.website_size_x > 2 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 2 and product.website_size_x > 3 and 'selected'"></td>
- </tr>
- <tr>
- <td t-att-class="product.website_size_y > 3 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 3 and product.website_size_x > 1 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 3 and product.website_size_x > 2 and 'selected'"></td>
- <td t-att-class="product.website_size_y > 3 and product.website_size_x > 3 and 'selected'"></td>
- </tr>
- </table>
- </a></li>
- </ul>
- </li>
- <li class='dropdown-submenu'>
- <a tabindex="-1" href="#">Styles</a>
- <ul class="dropdown-menu" name="style">
- <t t-foreach="styles" t-as="style">
- <li t-att-class="style_in_product(style, product) and 'active' or ''"><a href="#" t-att-data-id="style.id" t-att-data-class="style.html_class"><t t-esc="style.name"/></a></li>
- </t>
- </ul>
- </li>
- <li class='dropdown-submenu'>
- <a tabindex="-1" href="#">Promote</a>
- <ul class="dropdown-menu" name="sequence">
- <li><a href="#" class="js_go_to_top">Push to top</a></li>
- <li><a href="#" class="js_go_up">Push up</a>
- </li>
- <li><a href="#" class="js_go_down">Push down</a></li>
- <li><a href="#" class="js_go_to_bottom">Push to bottom</a></li>
- </ul>
- </li>
- </ul>
- </div>
- </div>
<t t-set="product_image_big" t-value="td_product['x']+td_product['y'] > 2"/>
<t t-call="website_sale.products_item"/>
</div>
<input type="hidden" t-if="len(product.product_variant_ids) == 1" name="product_id" t-att-value="product.product_variant_ids[0].id"/>
<t t-if="len(product.product_variant_ids) > 1">
<label label-default="label-default" class="radio" t-foreach="product.product_variant_ids" t-as="variant_id">
- <input type="radio" name="product_id" class="js_product_change" t-att-value="variant_id.id" t-att-data-lst_price="variant_id.lst_price" t-att-data-price="variant_id.price"/>
+ <input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else ''" t-att-value="variant_id.id" t-att-data-lst_price="variant_id.lst_price" t-att-data-price="variant_id.price"/>
<span t-esc="variant_id.name_get()[0][1]"/>
<span class="badge" t-if="variant_id.price_extra">
<t t-esc="variant_id.price_extra > 0 and '+' or ''"/><span t-field="variant_id.price_extra" style="white-space: nowrap;" t-field-options='{
<h2>Thank you for your order.</h2>
<div class="oe_website_sale_tx_status" t-att-data-order-id="order.id">
</div>
+ <h3 class="mt32"><strong>Order Details:</strong></h3>
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Products</th>
+ <th>Quantity</th>
+ <th class="text-right" width="100">Unit Price</th>
+ <th class="text-right" width="100">Subtotal</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="order.order_line" t-as="line">
+ <td>
+ <div>
+ <a t-attf-href="/shop/product/#{ slug(line.product_id.product_tmpl_id) }">
+ <strong t-esc="line.product_id.name_get()[0][1]"/>
+ </a>
+ </div>
+ <div class="text-muted" t-field="line.name"/>
+ </td>
+ <td>
+ <div id="quote_qty">
+ <span t-field="line.product_uom_qty"/>
+ <span t-field="line.product_uom"/>
+ </div>
+ </td>
+ <td>
+ <strong class="text-right">
+ <div t-field="line.price_unit"
+ t-field-options='{"widget": "monetary", "display_currency": "order.pricelist_id.currency_id"}'/>
+ </strong>
+ </td>
+ <td>
+ <div class="text-right"
+ t-field="line.price_subtotal"
+ t-field-options='{"widget": "monetary", "display_currency": "order.pricelist_id.currency_id"}'/>
+ </td>
+ </tr>
+ <tr>
+ <td></td><td></td>
+ <td class="text-right"><strong>Total:</strong></td>
+ <td class="text-right">
+ <strong t-field="order.amount_total"
+ t-field-options='{"widget": "monetary", "display_currency": "order.pricelist_id.currency_id"}'/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
<div class="clearfix"/>
<div class="oe_structure"/>
</div>
if field_type in FIELDS_TO_PGTYPES:
pg_type = (FIELDS_TO_PGTYPES[field_type], FIELDS_TO_PGTYPES[field_type])
elif issubclass(field_type, fields.float):
- if f.digits:
+ # Explicit support for "falsy" digits (0, False) to indicate a
+ # NUMERIC field with no fixed precision. The values will be saved
+ # in the database with all significant digits.
+ # FLOAT8 type is still the default when there is no precision because
+ # it is faster for most operations (sums, etc.)
+ if f.digits is not None:
pg_type = ('numeric', 'NUMERIC')
else:
pg_type = ('float8', 'DOUBLE PRECISION')
# check defaults
for k in cls._defaults:
assert k in cls._fields, \
- "Model %s has a default for nonexiting field %s" % (cls._name, k)
+ "Model %s has a default for non-existing field %s" % (cls._name, k)
# restart columns
for column in cls._columns.itervalues():
_schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
self._table, column['attname'])
- def _save_constraint(self, cr, constraint_name, type):
+ def _save_constraint(self, cr, constraint_name, type, definition):
"""
Record the creation of a constraint for this model, to make it possible
to delete it later when the module is uninstalled. Type can be either
return
assert type in ('f', 'u')
cr.execute("""
- SELECT 1 FROM ir_model_constraint, ir_module_module
+ SELECT type, definition FROM ir_model_constraint, ir_module_module
WHERE ir_model_constraint.module=ir_module_module.id
AND ir_model_constraint.name=%s
AND ir_module_module.name=%s
""", (constraint_name, self._module))
- if not cr.rowcount:
+ constraints = cr.dictfetchone()
+ if not constraints:
cr.execute("""
INSERT INTO ir_model_constraint
- (name, date_init, date_update, module, model, type)
+ (name, date_init, date_update, module, model, type, definition)
VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
(SELECT id FROM ir_module_module WHERE name=%s),
- (SELECT id FROM ir_model WHERE model=%s), %s)""",
- (constraint_name, self._module, self._name, type))
+ (SELECT id FROM ir_model WHERE model=%s), %s, %s)""",
+ (constraint_name, self._module, self._name, type, definition))
+ elif constraints['type'] != type or (definition and constraints['definition'] != definition):
+ cr.execute("""
+ UPDATE ir_model_constraint
+ SET date_update=now() AT TIME ZONE 'UTC', type=%s, definition=%s
+ WHERE name=%s AND module = (SELECT id FROM ir_module_module WHERE name=%s)""",
+ (type, definition, constraint_name, self._module))
def _save_relation_table(self, cr, relation_table):
"""
""" Create the foreign keys recorded by _auto_init. """
for t, k, r, d in self._foreign_keys:
cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (t, k, r, d))
- self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f')
+ self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f', False)
cr.commit()
del self._foreign_keys
for (key, con, _) in self._sql_constraints:
conname = '%s_%s' % (self._table, key)
- self._save_constraint(cr, conname, 'u')
- cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,))
- existing_constraints = cr.dictfetchall()
+ # using 1 to get result if no imc but one pgc
+ cr.execute("""SELECT definition, 1
+ FROM ir_model_constraint imc
+ RIGHT JOIN pg_constraint pgc
+ ON (pgc.conname = imc.name)
+ WHERE pgc.conname=%s
+ """, (conname, ))
+ existing_constraints = cr.dictfetchone()
sql_actions = {
'drop': {
'execute': False,
# constraint does not exists:
sql_actions['add']['execute'] = True
sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
- elif unify_cons_text(con) not in [unify_cons_text(item['condef']) for item in existing_constraints]:
+ elif unify_cons_text(con) != existing_constraints['definition']:
# constraint exists but its definition has changed:
sql_actions['drop']['execute'] = True
- sql_actions['drop']['msg_ok'] = sql_actions['drop']['msg_ok'] % (existing_constraints[0]['condef'].lower(), )
+ sql_actions['drop']['msg_ok'] = sql_actions['drop']['msg_ok'] % (existing_constraints['definition'] or '', )
sql_actions['add']['execute'] = True
sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
# we need to add the constraint:
+ self._save_constraint(cr, conname, 'u', unify_cons_text(con))
sql_actions = [item for item in sql_actions.values()]
sql_actions.sort(key=lambda x: x['order'])
for sql_action in [action for action in sql_actions if action['execute']]: