diff --git a/addons/product/product.py b/addons/product/product.py
index 6e1128985aa..8c74217efac 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -289,6 +289,15 @@ class product_template(osv.osv):
_name = "product.template"
_description = "Product Template"
+ def _get_image(self, cr, uid, ids, name, args, context=None):
+ result = dict.fromkeys(ids, False)
+ for obj in self.browse(cr, uid, ids, context=context):
+ result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
+ return result
+
+ def _set_image(self, cr, uid, id, name, value, args, context=None):
+ return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
+
_columns = {
'name': fields.char('Name', size=128, required=True, translate=True, select=True),
'product_manager': fields.many2one('res.users','Product Manager'),
@@ -328,6 +337,26 @@ class product_template(osv.osv):
'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
'seller_ids': fields.one2many('product.supplierinfo', 'product_id', 'Supplier'),
'company_id': fields.many2one('res.company', 'Company', select=1),
+ # image: all image fields are base64 encoded and PIL-supported
+ 'image': fields.binary("Image",
+ help="This field holds the image used as image for the product, limited to 1024x1024px."),
+ 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
+ string="Medium-sized image", type="binary", multi="_get_image",
+ store={
+ 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+ },
+ help="Medium-sized image of the product. It is automatically "\
+ "resized as a 128x128px image, with aspect ratio preserved, "\
+ "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
+ 'image_small': fields.function(_get_image, fnct_inv=_set_image,
+ string="Small-sized image", type="binary", multi="_get_image",
+ store={
+ 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+ },
+ help="Small-sized image of the product. It is automatically "\
+ "resized as a 64x64px image, with aspect ratio preserved. "\
+ "Use this field anywhere a small image is required."),
+ 'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Product Variants'),
}
def _get_uom_id(self, cr, uid, *args):
@@ -514,15 +543,6 @@ class product_product(osv.osv):
return result
- def _get_image(self, cr, uid, ids, name, args, context=None):
- result = dict.fromkeys(ids, False)
- for obj in self.browse(cr, uid, ids, context=context):
- result[obj.id] = tools.image_get_resized_images(obj.image, avoid_resize_medium=True)
- return result
-
- def _set_image(self, cr, uid, id, name, value, args, context=None):
- return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
-
def _get_name_template_ids(self, cr, uid, ids, context=None):
result = set()
template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)])
@@ -567,25 +587,6 @@ class product_product(osv.osv):
}, select=True),
'color': fields.integer('Color Index'),
- # image: all image fields are base64 encoded and PIL-supported
- 'image': fields.binary("Image",
- help="This field holds the image used as image for the product, limited to 1024x1024px."),
- 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
- string="Medium-sized image", type="binary", multi="_get_image",
- store={
- 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
- },
- help="Medium-sized image of the product. It is automatically "\
- "resized as a 128x128px image, with aspect ratio preserved, "\
- "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."),
- 'image_small': fields.function(_get_image, fnct_inv=_set_image,
- string="Small-sized image", type="binary", multi="_get_image",
- store={
- 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
- },
- help="Small-sized image of the product. It is automatically "\
- "resized as a 64x64px image, with aspect ratio preserved. "\
- "Use this field anywhere a small image is required."),
'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index bd3f7285126..0199c3cc1c5 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -27,6 +27,10 @@ def auth_method_public():
http.auth_methods['public'] = auth_method_public
+# PIL images have a type flag, but no MIME. Reverse type flag to MIME.
+PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
+# Completely arbitrary limits
+MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768)
class Website(openerp.addons.web.controllers.main.Home):
@http.route('/', type='http', auth="admin")
def index(self, **kw):
@@ -45,7 +49,8 @@ class Website(openerp.addons.web.controllers.main.Home):
newview = view.browse(request.cr, request.uid, newview_id, context={})
newview.write({
'arch': newview.arch.replace("website.default_page", path),
- 'name': "page/%s" % path
+ 'name': "page/%s" % path,
+ 'page': True,
})
if '.' in path:
module, idname = path.split('.')
@@ -145,15 +150,20 @@ class Website(openerp.addons.web.controllers.main.Home):
buf = cStringIO.StringIO(base64.decodestring(attachment.datas))
image = Image.open(buf)
- image.thumbnail((1024, 768), Image.ANTIALIAS)
+ mime = PIL_MIME_MAPPING[image.format]
- response = werkzeug.wrappers.Response(status=200, mimetype={
- 'PNG': 'image/png',
- 'JPEG': 'image/jpeg',
- 'GIF': 'image/gif',
- }[image.format])
+ w, h = image.size
+ resized = w > MAX_IMAGE_WIDTH or h > MAX_IMAGE_HEIGHT
+
+ # If saving unnecessary, just send the image buffer, don't go through
+ # Image.save() (especially as it breaks animated gifs)
+ if not resized:
+ buf.seek(0)
+ return werkzeug.wrappers.Response(buf, status=200, mimetype=mime)
+
+ image.thumbnail(IMAGE_LIMITS, Image.ANTIALIAS)
+ response = werkzeug.wrappers.Response(status=200, mimetype=mime)
image.save(response.stream, image.format)
-
return response
@http.route('/website/image', type='http', auth="public")
@@ -208,4 +218,8 @@ class Website(openerp.addons.web.controllers.main.Home):
return obj.website_published and "1" or "0"
+ @http.route(['/website/kanban/'], type='http', auth="public")
+ def kanban(self, **post):
+ return request.registry['website'].kanban_col(**post)
+
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css
index 246d62b62be..d446d14f3bc 100644
--- a/addons/website/static/src/css/editor.css
+++ b/addons/website/static/src/css/editor.css
@@ -1,4 +1,7 @@
-@charset "utf-8";
+.oe_hidden {
+ display: none !important;
+}
+
.oe_website_editorbar {
position: fixed;
top: 0;
@@ -54,3 +57,90 @@
z-index: 1;
display: none;
}
+
+.oe_snippets {
+ position: fixed;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ max-height: 280px;
+ min-height: 140px;
+ background: #282828;
+ box-shadow: 0px 10px 10px -10px black inset;
+ overflow-y: auto;
+}
+
+.oe_snippet {
+ display: inline-block;
+ vertical-align: top;
+ background: white;
+ width: 120px;
+ height: 120px;
+ border-radius: 3px;
+ margin: 10px;
+ margin-right: 0px;
+ cursor: move;
+ position: relative;
+ box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.2);
+ overflow: hidden;
+ -webkit-user-select: none;
+ user-select: none;
+}
+.oe_snippet .oe_snippet_thumbnail.oe_label {
+ text-align: center;
+ background: #747474;
+ background-image: radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+ height: 100%;
+ line-height: 120px;
+ color: white;
+ text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.95);
+}
+.oe_snippet .oe_snippet_body {
+ display: none;
+}
+.oe_snippet > * {
+ pointer-events: none;
+}
+.oe_snippet > *:first-child:not(.oe_snippet_thumbnail) {
+ display: block;
+ -webkit-transform-origin-x: 5px;
+ -webkit-transform-origin-y: 10px;
+ -webkit-transform: scale(0.095);
+}
+
+.oe_drop_zone {
+ display: block;
+ background: rgba(153, 0, 255, 0.17);
+ border-radius: 3px;
+ height: 32px;
+ margin: -16px 0px;
+ -webkit-transition: margin 250ms linear;
+}
+.oe_drop_zone:first-child:not(:last-child) {
+ margin-top: -8px;
+ margin-bottom: -24px;
+}
+.oe_drop_zone:last-child:not(:first-child) {
+ margin-top: -24px;
+ margin-bottom: -8px;
+}
+.oe_drop_zone.oe_hover {
+ background: rgba(0, 255, 133, 0.22);
+ z-index: 100000;
+}
+.oe_drop_zone:before {
+ content: "";
+ display: block;
+ border-top: dashed 2px rgba(209, 178, 255, 0.72);
+ position: relative;
+ top: 16px;
+}
+.oe_drop_zone:first-child:not(:last-child):before {
+ top: 8px;
+}
+.oe_drop_zone:last-child:not(:first-child):before {
+ top: 24px;
+}
+.oe_drop_zone.oe_hover:before {
+ border-top: dashed 2px rgba(116, 255, 161, 0.72);
+}
diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass
index 24a549f1d05..f7ce999257e 100644
--- a/addons/website/static/src/css/editor.sass
+++ b/addons/website/static/src/css/editor.sass
@@ -1,5 +1,8 @@
@charset "utf-8"
+.oe_hidden
+ display: none !important
+
.oe_website_editorbar
position: fixed
top: 0
@@ -47,3 +50,82 @@
white-space: nowrap
z-index: 1
display: none
+
+.oe_snippets
+ position: fixed
+ left: 0px
+ right: 0px
+ bottom: 0px
+ max-height: 280px
+ min-height: 140px
+ background: rgb(40,40,40)
+ box-shadow: 0px 10px 10px -10px black inset
+ overflow-y: auto
+
+.oe_snippet
+ display: inline-block
+ vertical-align: top
+ background: white
+ width: 120px
+ height: 120px
+ border-radius: 3px
+ margin: 10px
+ margin-right: 0px
+ cursor: move
+ position: relative
+ box-shadow: 0px 3px 10px rgba(0,0,0,0.2)
+ overflow: hidden
+ -webkit-user-select: none
+ user-select: none
+
+ .oe_snippet_thumbnail.oe_label
+ text-align: center
+ background: rgb(116, 116, 116)
+ background-image: radial-gradient(rgba(0,0,0,0.25),rgba(0,0,0,0.4))
+ height: 100%
+ line-height: 120px
+ color: white
+ text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.95)
+
+ .oe_snippet_body
+ display: none
+ & > *
+ pointer-events: none
+
+ & > *:first-child:not(.oe_snippet_thumbnail)
+ display: block
+ -webkit-transform-origin-x: 5px
+ -webkit-transform-origin-y: 10px
+ -webkit-transform: scale(0.095)
+
+
+.oe_drop_zone
+ display: block
+ background: rgba(153, 0, 255, 0.17)
+ border-radius: 3px
+ height: 32px
+ margin: -16px 0px
+ -webkit-transition: margin 250ms linear
+ &:first-child:not(:last-child)
+ margin-top: -8px
+ margin-bottom: -24px
+ &:last-child:not(:first-child)
+ margin-top: -24px
+ margin-bottom: -8px
+ &.oe_hover
+ background: rgba(0, 255, 133, 0.22)
+ z-index: 100000
+ &:before
+ content: ""
+ display: block
+ border-top: dashed 2px rgba(209, 178, 255, 0.72)
+ position: relative
+ top: 16px
+ &:first-child:not(:last-child):before
+ top: 8px
+ &:last-child:not(:first-child):before
+ top: 24px
+ &.oe_hover:before
+ border-top: dashed 2px rgba(116, 255, 161, 0.72)
+
+
diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js
index 2c589a49de5..620eab2a16b 100644
--- a/addons/website/static/src/js/website.js
+++ b/addons/website/static/src/js/website.js
@@ -1,430 +1,640 @@
-// Ugly. I'll clean this monday
-$(function () { if ($('html').attr('data-editable') !== '1') return;
+(function() {
+ "use strict";
-// TODO fme: put everything in openerp.website scope and load templates on
-// next tick or document ready
-// Also check with xmo if jquery.keypress.js is still needed.
+ var website = {};
+ // The following line can be removed in 2017
+ openerp.website = website;
-var doc_ready = $.Deferred();
-$(doc_ready.resolve);
+ var templates = [
+ '/website/static/src/xml/website.xml'
+ ];
-var templates = [
- '/website/static/src/xml/website.xml'
-];
-
-function loadTemplates(templates) {
- var def = $.Deferred();
- var count = templates.length;
- templates.forEach(function(t) {
- openerp.qweb.add_template(t, function(err) {
- if (err) {
- def.reject();
- } else {
- count--;
- if (count < 1) {
- def.resolve();
+ /* ----- TEMPLATE LOADING ---- */
+ website.add_template = function(template) {
+ templates.push(template);
+ };
+ website.load_templates = function(templates) {
+ var def = $.Deferred();
+ var count = templates.length;
+ templates.forEach(function(t) {
+ openerp.qweb.add_template(t, function(err) {
+ if (err) {
+ def.reject();
+ } else {
+ count--;
+ if (count < 1) {
+ def.resolve();
+ }
}
- }
+ });
});
- });
- return def;
-}
+ return def;
+ };
-var EditorBar = openerp.Widget.extend({
- template: 'Website.EditorBar',
- events: {
- 'click button[data-action=edit]': 'edit',
- 'click button[data-action=save]': 'save',
- 'click button[data-action=cancel]': 'cancel',
- 'click button[data-action=snippet]': 'snippet',
- },
- container: 'body',
- customize_setup: function() {
- var self = this;
- var view_name = $('html').data('view-xmlid');
- var menu = $('#customize-menu');
- this.$('#customize-menu-button').click(function(event) {
- menu.empty();
- openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': view_name }).then(
- function(result) {
- _.each(result, function (item) {
- if (item.header) {
- menu.append('
');
- } else {
- menu.append(_.str.sprintf(' %s ',
- item.id, item.active ? '' : '-empty', item.name));
- }
- });
- }
+ website.init_editor = function () {
+ var editor = new website.EditorBar();
+ editor.prependTo($('body'));
+ $('body').css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight());
+ };
+
+ /* ----- TOP EDITOR BAR FOR ADMIN ---- */
+ website.EditorBar = openerp.Widget.extend({
+ template: 'Website.EditorBar',
+ events: {
+ 'click button[data-action=edit]': 'edit',
+ 'click button[data-action=save]': 'save',
+ 'click button[data-action=cancel]': 'cancel',
+ 'click button[data-action=snippet]': 'snippet',
+ },
+ container: 'body',
+ customize_setup: function() {
+ var self = this;
+ var view_name = $('html').data('view-xmlid');
+ var menu = $('#customize-menu');
+ this.$('#customize-menu-button').click(function(event) {
+ menu.empty();
+ openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': view_name }).then(
+ function(result) {
+ _.each(result, function (item) {
+ if (item.header) {
+ menu.append('');
+ } else {
+ menu.append(_.str.sprintf(' %s ',
+ item.id, item.active ? '' : '-empty', item.name));
+ }
+ });
+ }
+ );
+ });
+ menu.on('click', 'a', function (event) {
+ var view_id = $(event.target).data('view-id');
+ openerp.jsonRpc('/website/customize_template_toggle', 'call', {
+ 'view_id': view_id
+ }).then( function(result) {
+ window.location.reload();
+ });
+ });
+ },
+ start: function() {
+ var self = this;
+
+ this.saving_mutex = new openerp.Mutex();
+
+ this.$('#website-top-edit').hide();
+ this.$('#website-top-view').show();
+
+ $('.dropdown-toggle').dropdown();
+ this.customize_setup();
+
+ this.$buttons = {
+ edit: this.$('button[data-action=edit]'),
+ save: this.$('button[data-action=save]'),
+ cancel: this.$('button[data-action=cancel]'),
+ snippet: this.$('button[data-action=snippet]'),
+ };
+
+ this.rte = new website.RTE(this);
+ this.rte.on('change', this, this.proxy('rte_changed'));
+
+ this.snippets = new website.Snippets();
+ this.snippets.appendTo($("body"));
+ window.snippets = this.snippets;
+
+ return $.when(
+ this._super.apply(this, arguments),
+ this.rte.insertBefore(this.$buttons.snippet.parent())
);
- });
- menu.on('click', 'a', function (event) {
- var view_id = $(event.target).data('view-id');
- openerp.jsonRpc('/website/customize_template_toggle', 'call', {
- 'view_id': view_id
- }).then( function(result) {
+ },
+ edit: function () {
+ this.$buttons.edit.prop('disabled', true);
+ this.$('#website-top-view').hide();
+ this.$('#website-top-edit').show();
+
+ // this.$buttons.cancel.add(this.$buttons.snippet).prop('disabled', false)
+ // .add(this.$buttons.save)
+ // .parent().show();
+ //
+ // TODO: span edition changing edition state (save button)
+ var $editables = $('[data-oe-model]')
+ .not('link, script')
+ // FIXME: propagation should make "meta" blocks non-editable in the first place...
+ .not('.oe_snippets,.oe_snippet, .oe_snippet *')
+ .prop('contentEditable', true)
+ .addClass('oe_editable');
+ var $rte_ables = $editables.not('[data-oe-type]');
+ var $raw_editables = $editables.not($rte_ables);
+
+ // temporary fix until we fix ckeditor
+ $raw_editables.each(function () {
+ $(this).parents().add($(this).find('*')).on('click', function(ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ });
+ });
+
+ this.rte.start_edition($rte_ables);
+ $raw_editables.on('keydown keypress cut paste', function (e) {
+ var $target = $(e.target);
+ if ($target.hasClass('oe_dirty')) {
+ return;
+ }
+
+ $target.addClass('oe_dirty');
+ this.$buttons.save.prop('disabled', false);
+ }.bind(this));
+ },
+ rte_changed: function () {
+ this.$buttons.save.prop('disabled', false);
+ },
+ save: function () {
+ var self = this;
+ var defs = [];
+ $('.oe_dirty').each(function (i, v) {
+ var $el = $(this);
+ // TODO: Add a queue with concurrency limit in webclient
+ // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
+ var def = self.saving_mutex.exec(function () {
+ return self.saveElement($el).then(function () {
+ $el.removeClass('oe_dirty');
+ }).fail(function () {
+ var data = $el.data();
+ console.error(_.str.sprintf('Could not save %s#%d#%s', data.oeModel, data.oeId, data.oeField));
+ });
+ });
+ defs.push(def);
+ });
+ return $.when.apply(null, defs).then(function () {
window.location.reload();
});
- });
- },
- start: function() {
- var self = this;
-
- this.saving_mutex = new openerp.Mutex();
-
- this.$('#website-top-edit').hide();
- this.$('#website-top-view').show();
-
- $('.dropdown-toggle').dropdown();
- this.customize_setup();
-
- this.$buttons = {
- edit: this.$('button[data-action=edit]'),
- save: this.$('button[data-action=save]'),
- cancel: this.$('button[data-action=cancel]'),
- snippet: this.$('button[data-action=snippet]'),
- };
-
- self.snippet_start();
-
- this.rte = new RTE(this);
- this.rte.on('change', this, this.proxy('rte_changed'));
-
- return $.when(
- this._super.apply(this, arguments),
- this.rte.insertBefore(this.$buttons.snippet.parent())
- );
- },
- edit: function () {
- this.$buttons.edit.prop('disabled', true);
- this.$('#website-top-view').hide();
- this.$('#website-top-edit').show();
-
- // this.$buttons.cancel.add(this.$buttons.snippet).prop('disabled', false)
- // .add(this.$buttons.save)
- // .parent().show();
- //
- // TODO: span edition changing edition state (save button)
- var $editables = $('[data-oe-model]')
- .not('link, script')
- // FIXME: propagation should make "meta" blocks non-editable in the first place...
- .not('.oe_snippet_editor')
- .prop('contentEditable', true)
- .addClass('oe_editable');
- var $rte_ables = $editables.not('[data-oe-type]');
- var $raw_editables = $editables.not($rte_ables);
-
- // temporary fix until we fix ckeditor
- $raw_editables.each(function () {
- $(this).parents().add($(this).find('*')).on('click', function(ev) {
- ev.preventDefault();
- ev.stopPropagation();
- });
- });
-
- this.rte.start_edition($rte_ables);
- $raw_editables.on('keydown keypress cut paste', function (e) {
- var $target = $(e.target);
- if ($target.hasClass('oe_dirty')) {
- return;
+ },
+ saveElement: function ($el) {
+ var data = $el.data();
+ var html = $el.html();
+ var xpath = data.oeXpath;
+ if (xpath) {
+ var $w = $el.clone();
+ $w.removeClass('oe_dirty');
+ _.each(['model', 'id', 'field', 'xpath'], function(d) {$w.removeAttr('data-oe-' + d);});
+ $w
+ .removeClass('oe_editable')
+ .prop('contentEditable', false);
+ html = $w.wrap('').parent().html();
}
-
- $target.addClass('oe_dirty');
- this.$buttons.save.prop('disabled', false);
- }.bind(this));
- },
- rte_changed: function () {
- this.$buttons.save.prop('disabled', false);
- },
- save: function () {
- var self = this;
- var defs = [];
- $('.oe_dirty').each(function (i, v) {
- var $el = $(this);
- // TODO: Add a queue with concurrency limit in webclient
- // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
- var def = self.saving_mutex.exec(function () {
- return self.saveElement($el).then(function () {
- $el.removeClass('oe_dirty');
- }).fail(function () {
- var data = $el.data();
- console.error(_.str.sprintf('Could not save %s#%d#%s', data.oeModel, data.oeId, data.oeField));
- });
+ return openerp.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.ui.view',
+ method: 'save',
+ args: [data.oeModel, data.oeId, data.oeField, html, xpath]
});
- defs.push(def);
- });
- return $.when.apply(null, defs).then(function () {
+ },
+ cancel: function () {
window.location.reload();
- });
- },
- saveElement: function ($el) {
- var data = $el.data();
- var html = $el.html();
- var xpath = data.oeXpath;
- if (xpath) {
- var $w = $el.clone();
- $w.removeClass('oe_dirty');
- _.each(['model', 'id', 'field', 'xpath'], function(d) {$w.removeAttr('data-oe-' + d);});
- $w
- .removeClass('oe_editable')
- .prop('contentEditable', false);
- html = $w.wrap('
').parent().html();
- }
- return openerp.jsonRpc('/web/dataset/call', 'call', {
- model: 'ir.ui.view',
- method: 'save',
- args: [data.oeModel, data.oeId, data.oeField, html, xpath]
- });
- },
- cancel: function () {
- window.location.reload();
- },
- setup_droppable: function () {
- var self = this;
- $('.oe_snippet_drop').remove();
- var droppable = '
';
- var $zone = $(':not(.oe_snippet) > .container');
- $zone.after(droppable);//.after(droppable);
-
- $(".oe_snippet_drop").droppable({
- hoverClass: 'oe_accepting',
- drop: function( event, ui ) {
- console.log(event, ui, "DROP");
-
- $(event.target).replaceWith($(ui.draggable).html());
- $('.oe_selected').remove();
- $('.oe_snippet_drop').remove();
- }
- });
- },
- snippet_start: function () {
- var self = this;
- $('.oe_snippet').draggable().click(function(ev) {
- self.setup_droppable();
- $(".oe_snippet_drop").show();
- $('.oe_selected').removeClass('oe_selected');
- $(ev.currentTarget).addClass('oe_selected');
- });
-
- },
- snippet: function (ev) {
- $('.oe_snippet_editor').toggle();
- },
-});
-
-var RTE = openerp.Widget.extend({
- tagName: 'li',
- id: 'oe_rte_toolbar',
- className: 'oe_right oe_rte_toolbar',
- // editor.ui.items -> possible commands &al
- // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
-
- start_edition: function ($elements) {
- var self = this;
- this.snippet_carousel();
- $elements
- .not('span, [data-oe-type]')
- .each(function () {
- var $this = $(this);
- CKEDITOR.inline(this, self._config()).on('change', function () {
- $this.addClass('oe_dirty');
- self.trigger('change', this, null);
- });
- });
- },
-
- _current_editor: function () {
- return CKEDITOR.currentInstance;
- },
- _config: function () {
- var removed_plugins = [
- // remove custom context menu
- 'contextmenu,tabletools,liststyle',
- // magicline captures mousein/mouseout => draggable does not work
- 'magicline'
- ];
- return {
- // Disable auto-generated titles
- // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
- title: false,
- removePlugins: removed_plugins.join(','),
- uiColor: '',
- // Ensure no config file is loaded
- customConfig: '',
- // Disable ACF
- allowedContent: true,
- // Don't insert paragraphs around content in e.g.
- autoParagraph: false,
- filebrowserImageUploadUrl: "/website/attach",
- // Support for sharedSpaces in 4.x
- extraPlugins: 'sharedspace',
- // Place toolbar in controlled location
- sharedSpaces: { top: 'oe_rte_toolbar' },
- toolbar: [
- {name: 'basicstyles', items: [
- "Bold", "Italic", "Underline", "Strike", "Subscript",
- "Superscript", "TextColor", "BGColor", "RemoveFormat"
- ]},{
- name: 'span', items: [
- "Link", "Unlink", "Blockquote", "BulletedList",
- "NumberedList", "Indent", "Outdent",
- ]},{
- name: 'justify', items: [
- "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
- ]},{
- name: 'special', items: [
- "Image", "Table"
- ]},{
- name: 'styles', items: [
- "Format", "Styles"
- ]}
- ],
- // styles dropdown in toolbar
- stylesSet: [
- // emphasis
- {name: "Muted", element: 'span', attributes: {'class': 'text-muted'}},
- {name: "Primary", element: 'span', attributes: {'class': 'text-primary'}},
- {name: "Warning", element: 'span', attributes: {'class': 'text-warning'}},
- {name: "Danger", element: 'span', attributes: {'class': 'text-danger'}},
- {name: "Success", element: 'span', attributes: {'class': 'text-success'}},
- {name: "Info", element: 'span', attributes: {'class': 'text-info'}}
- ],
- };
- },
- // TODO clean
- snippet_carousel: function () {
- var self = this;
- $('.carousel .js_carousel_options .label').on('click', function (e) {
- e.preventDefault();
- var $button = $(e.currentTarget);
- var $c = $button.parents(".carousel:first");
-
- if($button.hasClass("js_add")) {
- var cycle = $c.find(".carousel-inner .item").size();
- $c.find(".carousel-inner").append(QWeb.render("Website.Snipped.carousel"));
- $c.carousel(cycle);
- }
- else {
- var cycle = $c.find(".carousel-inner .item.active").remove();
- $c.find(".carousel-inner .item:first").addClass("active");
- $c.carousel(0);
- self.trigger('change', self, null);
- }
- });
- $('.carousel .js_carousel_options').show();
- }
-});
-
-$.when(doc_ready, loadTemplates(templates)).then(function() {
- var editor = new EditorBar();
- editor.prependTo($('body'));
- $('body').css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight());
-
- // TODO: Create an openerp.Widget out of this
-
- function make_static(){
- $('.oe_snippet_demo').removeClass('oe_new');
- $('.oe_page *').off('mouseover mouseleave');
- $('.oe_page .oe_selected').removeClass('oe_selected');
- }
-
- var selected_snippet = null;
- function snippet_click(event){
- if(selected_snippet){
- selected_snippet.removeClass('oe_selected');
- if(selected_snippet[0] === $(this)[0]){
- selected_snippet = null;
- event.preventDefault();
- make_static();
- return;
- }
- }
- $(this).addClass('oe_selected');
- selected_snippet = $(this);
- make_editable();
- event.preventDefault();
- }
- //$('.oe_snippet').click(snippet_click);
-
- var hover_element = null;
-
- function make_editable( constraint_after, constraint_inside ){
- if(selected_snippet && selected_snippet.hasClass('oe_new')){
- $('.oe_snippet_demo').addClass('oe_new');
- }else{
- $('.oe_snippet_demo').removeClass('oe_new');
- }
-
- $('.oe_page *').off('mouseover');
- $('.oe_page *').off('mouseleave');
- $('.oe_page *').mouseover(function(event){
- console.log('hover:',this);
- if(hover_element){
- hover_element.removeClass('oe_selected');
- hover_element.off('click');
- }
- $(this).addClass('oe_selected');
- $(this).click(append_snippet);
- hover_element = $(this);
- event.stopPropagation();
- });
- $('.oe_page *').mouseleave(function(){
- if(hover_element && $(this) === hover_element){
- hover_element = null;
- $(this).removeClass('oe_selected');
- }
- });
- }
-
- function customier_option_get(event){
-
-
- event.preventDefault();
- }
-
- function append_snippet(event){
- console.log('click',this,event.button);
- if(event.button === 0){
- if(selected_snippet){
- if(selected_snippet.hasClass('oe_new')){
- var new_snippet = $("
");
- new_snippet.append($(this).clone());
- new_snippet.click(snippet_click);
- $('.oe_snippet.oe_selected').before(new_snippet);
- }else{
- $(this).after($('.oe_snippet.oe_selected').contents().clone());
- }
- selected_snippet.removeClass('oe_selected');
- selected_snippet = null;
- make_static();
- }
- }else if(event.button === 1){
- $(this).remove();
- }
- event.preventDefault();
- }
-});
-
-});
-// }()); I'll clean this monday
-
-
-$(function () {
- $(document).on('click', '.js_publish, .js_unpublish', function (e) {
- e.preventDefault();
- var $link = $(this).parent();
- $link.find('.js_publish, .js_unpublish').addClass("hidden");
- var $unp = $link.find(".js_unpublish");
- var $p = $link.find(".js_publish");
- $.post('/website/publish', {'id': $link.data('id'), 'object': $link.data('object')}, function (result) {
- if (+result) {
- $p.addClass("hidden");
- $unp.removeClass("hidden");
- } else {
- $p.removeClass("hidden");
- $unp.addClass("hidden");
- }
- });
+ },
+ snippet: function (ev) {
+ this.snippets.toggle();
+ },
});
-});
+
+ /* ----- RICH TEXT EDITOR ---- */
+ website.RTE = openerp.Widget.extend({
+ tagName: 'li',
+ id: 'oe_rte_toolbar',
+ className: 'oe_right oe_rte_toolbar',
+ // editor.ui.items -> possible commands &al
+ // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
+
+ start_edition: function ($elements) {
+ var self = this;
+ this.snippet_carousel();
+ $elements
+ .not('span, [data-oe-type]')
+ .each(function () {
+ var $this = $(this);
+ CKEDITOR.inline(this, self._config()).on('change', function () {
+ $this.addClass('oe_dirty');
+ self.trigger('change', this, null);
+ });
+ });
+ },
+
+ _current_editor: function () {
+ return CKEDITOR.currentInstance;
+ },
+ _config: function () {
+ var removed_plugins = [
+ // remove custom context menu
+ 'contextmenu,tabletools,liststyle',
+ // magicline captures mousein/mouseout => draggable does not work
+ 'magicline'
+ ];
+ return {
+ // Disable auto-generated titles
+ // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
+ title: false,
+ removePlugins: removed_plugins.join(','),
+ uiColor: '',
+ // Ensure no config file is loaded
+ customConfig: '',
+ // Disable ACF
+ allowedContent: true,
+ // Don't insert paragraphs around content in e.g.
+ autoParagraph: false,
+ filebrowserImageUploadUrl: "/website/attach",
+ // Support for sharedSpaces in 4.x
+ extraPlugins: 'sharedspace',
+ // Place toolbar in controlled location
+ sharedSpaces: { top: 'oe_rte_toolbar' },
+ toolbar: [
+ {name: 'basicstyles', items: [
+ "Bold", "Italic", "Underline", "Strike", "Subscript",
+ "Superscript", "TextColor", "BGColor", "RemoveFormat"
+ ]},{
+ name: 'span', items: [
+ "Link", "Unlink", "Blockquote", "BulletedList",
+ "NumberedList", "Indent", "Outdent"
+ ]},{
+ name: 'justify', items: [
+ "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
+ ]},{
+ name: 'special', items: [
+ "Image", "Table"
+ ]},{
+ name: 'styles', items: [
+ "Format", "Styles"
+ ]}
+ ],
+ // styles dropdown in toolbar
+ stylesSet: [
+ // emphasis
+ {name: "Muted", element: 'span', attributes: {'class': 'text-muted'}},
+ {name: "Primary", element: 'span', attributes: {'class': 'text-primary'}},
+ {name: "Warning", element: 'span', attributes: {'class': 'text-warning'}},
+ {name: "Danger", element: 'span', attributes: {'class': 'text-danger'}},
+ {name: "Success", element: 'span', attributes: {'class': 'text-success'}},
+ {name: "Info", element: 'span', attributes: {'class': 'text-info'}}
+ ],
+ };
+ },
+ // TODO clean
+ snippet_carousel: function () {
+ var self = this;
+ $('.carousel .js_carousel_options .label').on('click', function (e) {
+ e.preventDefault();
+ var $button = $(e.currentTarget);
+ var $c = $button.parents(".carousel:first");
+
+ if($button.hasClass("js_add")) {
+ var cycle = $c.find(".carousel-inner .item").size();
+ $c.find(".carousel-inner").append(QWeb.render("Website.Snipped.carousel"));
+ $c.carousel(cycle);
+ }
+ else {
+ var cycle = $c.find(".carousel-inner .item.active").remove();
+ $c.find(".carousel-inner .item:first").addClass("active");
+ $c.carousel(0);
+ self.trigger('change', self, null);
+ }
+ });
+ $('.carousel .js_carousel_options').show();
+ }
+ });
+
+ /* ----- SNIPPET SELECTOR ---- */
+ website.Snippets = openerp.Widget.extend({
+ template: 'website.snippets',
+ init: function () {
+ this._super.apply(this, arguments);
+ },
+ start: function() {
+ var self = this;
+
+ $.ajax({
+ type: "GET",
+ url: "/page/website.snippets",
+ dataType: "text",
+ success: function(snippets){
+ self.$el.html(snippets);
+ self.start_snippets();
+ },
+ });
+
+ },
+ // setup widget and drag and drop
+ start_snippets: function(){
+ var self = this;
+
+ this.$('.oe_snippet').draggable({
+ helper: 'clone',
+ appendTo: 'body',
+ start: function(){
+ var snippet = $(this);
+
+ self.activate_drop_zones({
+ siblings: snippet.data('selector-siblings'),
+ childs: snippet.data('selector-childs')
+ });
+
+ $('.oe_drop_zone').droppable({
+ over: function(){
+ // FIXME: stupid hack to prevent multiple droppable to activate at once ...
+ // it's not even working properly but it's better than nothing.
+ $(".oe_drop_zone.oe_hover").removeClass("oe_hover");
+ $(this).addClass("oe_hover");
+ },
+ out: function(){
+ $(this).removeClass("oe_hover");
+ },
+ drop: function(){
+ $(".oe_drop_zone.oe_hover")
+ .replaceWith(snippet.find('.oe_snippet_body').clone())
+ .removeClass('oe_snippet_body');
+ },
+ });
+ },
+ stop: function(){
+ self.deactivate_drop_zones();
+ },
+ });
+ },
+ // A generic drop zone generator. two css selectors can be provided
+ // selector.childs -> will insert drop zones as direct child of the selected elements
+ // in case the selected elements have children themselves, dropzones will be interleaved
+ // with them.
+ // selector.siblings -> will insert drop zones after and before selected elements
+ activate_drop_zones: function(selector){
+ var self = this;
+ var child_selector = selector.childs || '';
+ var sibling_selector = selector.siblings || '';
+ var zone_template = "
";
+
+ $('.oe_drop_zone').remove();
+
+ if(child_selector){
+ var $zones = $(child_selector);
+ for( var i = 0, len = $zones.length; i < len; i++ ){
+ $zones.eq(i).find('> *:not(.oe_drop_zone)').after(zone_template);
+ $zones.eq(i).prepend(zone_template);
+ }
+ }
+
+ if(sibling_selector){
+ var $zones = $(sibling_selector);
+ for( var i = 0, len = $zones.length; i < len; i++ ){
+ if($zones.eq(i).prev('.oe_drop_zone').length === 0){
+ $zones.eq(i).before(zone_template);
+ }
+ if($zones.eq(i).next('.oe_drop_zone').length === 0){
+ $zones.eq(i).after(zone_template);
+ }
+ }
+ }
+
+ // Cleaning up unnecessary zones
+ $('.oe_snippets .oe_drop_zone').remove(); // no zone in the snippet selector ...
+ $('#website-top-view .oe_drop_zone').remove(); // no zone in the top bars ...
+ $('#website-top-edit .oe_drop_zone').remove();
+ var count;
+ do {
+ count = 0;
+ var $zones = $('.oe_drop_zone + .oe_drop_zone'); // no two consecutive zones
+ count += $zones.length;
+ $zones.remove();
+
+ $zones = $('.oe_drop_zone > .oe_drop_zone').remove(); // no recusrive zones
+ count += $zones.length;
+ $zones.remove();
+ }while(count > 0);
+
+ // Cleaning up zones placed between floating or inline elements
+ var $zones = $('.oe_drop_zone');
+ for( var i = 0, len = $zones.length; i < len; i++ ){
+ var zone = $zones.eq(i);
+ var prev = zone.prev();
+ var next = zone.next();
+ var float_prev = zone.prev().css('float') || 'none';
+ var float_next = zone.next().css('float') || 'none';
+ var disp_prev = zone.prev().css('display') || null;
+ var disp_next = zone.next().css('display') || null;
+ if( (float_prev === 'left' || float_prev === 'right')
+ && (float_next === 'left' || float_next === 'right') ){
+ zone.remove();
+ continue;
+ }else if( !( disp_prev === null
+ || disp_next === null
+ || disp_prev === 'block'
+ || disp_next === 'block'
+ ) ){
+ zone.remove();
+ continue;
+ }
+ }
+ },
+ deactivate_drop_zones: function(){
+ $('.oe_drop_zone').remove();
+ },
+ toggle: function(){
+ if(this.$el.hasClass('oe_hidden')){
+ this.$el.removeClass('oe_hidden');
+ }else{
+ this.$el.addClass('oe_hidden');
+ }
+ },
+ snippet_start: function () {
+ var self = this;
+ $('.oe_snippet').draggable().click(function(ev) {
+ self.setup_droppable();
+ $(".oe_snippet_drop").show();
+ $('.oe_selected').removeClass('oe_selected');
+ $(ev.currentTarget).addClass('oe_selected');
+ });
+
+ },
+ });
+
+ function noop() {}
+ var alter_dialog = {
+ image: function (definition) {
+ definition.removeContents('Link');
+ definition.removeContents('advanced');
+
+ var upload = definition.getContents('Upload');
+ upload.add({
+ type: 'select',
+ label: 'Existing attachments',
+ id: 'ir_attachment',
+ items: [['']],
+ /**
+ * On dialog load, fetch all attachments (on ir.ui.view =>
+ * previously uploaded images) and add them to the select's
+ * options (items array & add method)
+ */
+ onLoad: function () {
+ var field = this;
+
+ // FIXME: fuck this garbage, also fuck openerp.Model
+ return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'ir.attachment',
+ method: 'search_read',
+ args: [],
+ kwargs: {
+ fields: ['name'],
+ domain: [['res_model', '=', 'ir.ui.view']],
+ order: 'name',
+ }
+ }).then(function (results) {
+ _(results).each(function (result) {
+ field.add(result.name, result.id);
+ });
+ });
+ },
+ /**
+ * The image widgets uses "txtUrl" to do most of its stuff.
+ * Synchronize select & txtUrl by generating the correct URL
+ * and setting it there
+ */
+ onChange: function () {
+ var id = this.getValue();
+ var url = this.getDialog().getContentElement('info', 'txtUrl');
+ if (!id) {
+ url.setValue('');
+ return;
+ }
+
+ url.setValue('/website/attachment/' + id);
+ },
+ });
+ // Override uploadButton to send its information to the select
+ // created above instead of directly to txtUrl. The select will
+ // propagate to txtUrl
+ upload.get('uploadButton').filebrowser = {
+ onSelect: function (url) {
+ var id = url.split('/').pop();
+ var attachments = this.getDialog().getContentElement('Upload', 'ir_attachment');
+ // TODO: return supplementary info to get image/attachment name?
+ attachments.add(id, id);
+ attachments.setValue(id);
+ }
+ };
+
+ var old_show = definition.onShow;
+ definition.onShow = function () {
+ // CKEDITOR does not *override* onShow, is smashes the existing
+ // one instead, so override "by hand"
+ if (old_show) {
+ old_show.call(this);
+ }
+ // Assloads of code in the image plugin just go and tear into
+ // the info tab without a care, so can't just remove the tab or
+ // its content. Hide the tab instead, the effect is roughly the
+ // same.
+ this.hidePage('info');
+ // Force the dialog to always and only display the Upload tab
+ this.selectPage('Upload');
+ this.on('selectPage', function (e) {
+ setTimeout(function () {
+ if (e.data.page !== 'Upload') {
+ this.selectPage('Upload');
+ }
+ }.bind(this), 0);
+ });
+ }
+ },
+ link: function (definition) {
+ definition.removeContents('target');
+ definition.removeContents('advanced');
+
+ var info = definition.getContents('info');
+ info.remove('linkType');
+ info.remove('anchorOptions');
+ info.remove('emailOptions');
+
+ info.get('urlOptions').children[0].widths = [ '0%', '100%' ];
+ info.get('protocol').style = 'display: none';
+ // TODO: sync edition of url to website_pages?
+ info.add({
+ type: 'select',
+ label: "Existing page",
+ id: 'website_pages',
+ items: [['']],
+ /**
+ * onload fetch all the pages existing in the website, then
+ * display that.
+ */
+ onLoad: function () {
+ var field = this;
+
+ return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'website',
+ method: 'list_pages',
+ args: [],
+ kwargs: {}
+ }).then(function (results) {
+ _(results).each(function (result) {
+ field.add(result.name, result.url);
+ });
+ });
+ },
+ onChange: function () {
+ var url = this.getValue();
+ var url_field = this.getDialog().getContentElement('info', 'url');
+ if (!url) {
+ url_field.setValue('');
+ return;
+ }
+
+ url_field.setValue(url);
+ }
+ })
+ }
+ };
+ CKEDITOR.on('dialogDefinition', function (ev) {
+ (alter_dialog[ev.data.name] || noop)(ev.data.definition);
+ });
+
+ var all_ready = null;
+ var dom_ready = $.Deferred();
+ $(dom_ready.resolve);
+
+ /**
+ * Returns a deferred resolved when the templates are loaded
+ * and the Widgets can be instanciated.
+ */
+ website.ready = function() {
+ if (!all_ready) {
+ all_ready = dom_ready.then(function () {
+ // TODO: load translations
+ return website.load_templates(templates);
+ });
+ }
+ return all_ready;
+ };
+
+ dom_ready.then(function () {
+ website.is_editable = $('html').attr('data-editable') === '1';
+
+ if (website.is_editable) {
+ website.ready().then(website.init_editor);
+ }
+
+ /* ----- PUBLISHING STUFF ---- */
+ $(document).on('click', '.js_publish, .js_unpublish', function (e) {
+ e.preventDefault();
+ var $link = $(this).parent();
+ $link.find('.js_publish, .js_unpublish').addClass("hidden");
+ var $unp = $link.find(".js_unpublish");
+ var $p = $link.find(".js_publish");
+ $.post('/website/publish', {'id': $link.data('id'), 'object': $link.data('object')}, function (result) {
+ if (+result) {
+ $p.addClass("hidden");
+ $unp.removeClass("hidden");
+ } else {
+ $p.removeClass("hidden");
+ $unp.addClass("hidden");
+ }
+ });
+ });
+
+ });
+
+ return website;
+})();
diff --git a/addons/website/static/src/xml/website.xml b/addons/website/static/src/xml/website.xml
index 9f11ff40bca..e6611121bb8 100644
--- a/addons/website/static/src/xml/website.xml
+++ b/addons/website/static/src/xml/website.xml
@@ -57,4 +57,10 @@
+
+
+
+
+
+
diff --git a/addons/website/views/views.xml b/addons/website/views/views.xml
index 509f0180f5d..42a6e2add3c 100644
--- a/addons/website/views/views.xml
+++ b/addons/website/views/views.xml
@@ -6,12 +6,12 @@
-
+
-
+
@@ -27,22 +27,22 @@
-
-
-
+
+
+
+
+
+
-
-
@@ -122,107 +122,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
Wildlife Analysis
- In a beautiful place out in the country
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipisicing elit,
- sed do eiusmod tempor incididunt ut labore et dolore magna
- aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
- Duis aute irure dolor in reprehenderit in voluptate velit
- esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum
-
-
-
-
-
-
-
-
-
-
-
Niagara
-
- Lorem ipsum dolor sit amet, consectetur adipisicing elit,
- sed do eiusmod tempor incididunt ut labore et dolore magna
- aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
-
-
Whitewater
-
- Duis aute irure dolor in reprehenderit in voluptate velit
- esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum
-
-
-
-
Red Moss
-
- Sed ut perspiciatis unde omnis iste natus error sit voluptatem
- accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
- quae ab illo inventore veritatis et quasi architecto beatae vitae
- dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas
-
-
-
-
-
-
-
-
-
-
-
Wildlife Analysis
-
In a beautiful place out in the country
-
- Lorem ipsum dolor sit amet, consectetur adipisicing elit,
- sed do eiusmod tempor incididunt ut labore et dolore magna
- aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat.
-
-
- Duis aute irure dolor in reprehenderit in voluptate velit
- esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum
-
-
-
-
-
-
-
-
-
-
-
-
-