diff --git a/web_widget_slickroom/README.rst b/web_widget_slickroom/README.rst
new file mode 100644
index 00000000..9cb99875
--- /dev/null
+++ b/web_widget_slickroom/README.rst
@@ -0,0 +1,94 @@
+.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
+ :target: http://www.gnu.org/licenses/agpl
+ :alt: License: AGPL-3
+
+===================================================
+Slick Carousel Widget with DarkroomJS Image Editing
+===================================================
+
+This module extends the `Slick`_ carousel widget provided by
+`web_widget_slick` to include the `DarkroomJS`_ image editing features provided
+by `web_widget_darkroom`.
+
+.. _Slick: http://kenwheeler.github.io/slick
+.. _DarkroomJS: https://github.com/MattKetmo/darkroomjs
+
+Usage
+=====
+
+To create a Slick carousel widget with DarkroomJS support, follow the
+usage instructions in the `web_widget_slick` documentation, but replace
+"one2many_slick_images" with "slickroom" in the field definition, as shown
+here::
+
+
+
+To edit an image in a carousel, simply click the Edit button in the form view,
+then click on the image you wish to edit to open a DarkroomJS modal. Edit the
+image as desired according to the `web_widget_darkroom` documentation, and
+click Save to save the changes and update the carousel.
+
+Example Module
+--------------
+
+An example implementation, for instructional purposes as well as convenient
+functional testing, is provided in the `web_widget_slick_example` module.
+
+* Install `web_widget_slick_example`.
+* Activate Developer Mode.
+* Go to Settings / Technical / Slick, and open the record.
+* The standard Slick carousel widget (from `web_widget_slick`) is displayed on
+ top, followed by the slickroom widget with DarkroomJS support. Click the Edit
+ button in the form view to try out the DarkroomJS features.
+
+To try out different Slick settings:
+
+* Go to Settings/User Interface/Views and search for 'slick.example.view.form'.
+* Open the form view record.
+* Click the Edit button.
+* In the Architecture editor, find `options="{'slidesToShow': 2}`, and add
+ any desired settings (separated by commas) inside the curly braces.
+* Save the changes and browse to the widget, as described above, to see the
+ widget with the new settings in effect.
+
+.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
+ :alt: Try me on Runbot
+ :target: https://runbot.odoo-community.org/runbot/162/10.0
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues
+`_. In case of trouble, please
+check there if your issue has already been reported. If you spotted it first,
+help us smash it by providing detailed and welcomed feedback.
+
+Credits
+=======
+
+Images
+------
+
+* Odoo Community Association: `Icon `_.
+
+Contributors
+------------
+
+* Brent Hughes
+
+Do not contact contributors directly about support or help with technical issues.
+
+Maintainer
+----------
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+This module is maintained by the OCA.
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+To contribute to this module, please visit https://odoo-community.org.
diff --git a/web_widget_slickroom/__init__.py b/web_widget_slickroom/__init__.py
new file mode 100644
index 00000000..25e7ed3c
--- /dev/null
+++ b/web_widget_slickroom/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 LasLabs Inc.
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
diff --git a/web_widget_slickroom/__manifest__.py b/web_widget_slickroom/__manifest__.py
new file mode 100644
index 00000000..1cc5b16e
--- /dev/null
+++ b/web_widget_slickroom/__manifest__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 LasLabs Inc.
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+{
+ "name": "Slick Carousel Widget with DarkroomJS Image Editing",
+ "summary": "Provides Slick Carousel Widget with DarkroomJS image editing",
+ "version": "10.0.1.0.0",
+ "category": "Web",
+ "website": "https://laslabs.com/",
+ "author": "LasLabs, Odoo Community Association (OCA)",
+ "license": "LGPL-3",
+ "application": False,
+ "installable": True,
+ "depends": [
+ "web_widget_darkroom",
+ "web_widget_slick",
+ ],
+ "data": [
+ "templates/assets.xml",
+ ],
+}
diff --git a/web_widget_slickroom/static/description/icon.png b/web_widget_slickroom/static/description/icon.png
new file mode 100644
index 00000000..3a0328b5
Binary files /dev/null and b/web_widget_slickroom/static/description/icon.png differ
diff --git a/web_widget_slickroom/static/src/js/web_widget_slickroom.js b/web_widget_slickroom/static/src/js/web_widget_slickroom.js
new file mode 100644
index 00000000..4ec6750e
--- /dev/null
+++ b/web_widget_slickroom/static/src/js/web_widget_slickroom.js
@@ -0,0 +1,67 @@
+/* Copyright 2017 LasLabs Inc.
+ * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */
+
+odoo.define('web_widget_slickroom', function (require) {
+ "use strict";
+
+ var core = require('web.core');
+ var FieldSlickImages = require('web_widget_slick').FieldSlickImages;
+
+ var FieldSlickroomImages = FieldSlickImages.extend({
+
+ widget_class: FieldSlickImages.prototype.widget_class + ' o_slickroom',
+ events: _.extend({}, FieldSlickImages.prototype.events, {
+ 'click img': '_openModal'
+ }),
+
+ _openModal: function (ev) {
+ if (this.get("effective_readonly")) {
+ return;
+ }
+
+ var recordId = $(ev.target).data('record-id');
+ var modalAction = {
+ type: 'ir.actions.act_window',
+ res_model: 'darkroom.modal',
+ name: 'Darkroom',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ active_field: this.options.fieldName,
+ active_model: this.options.modelName,
+ active_record_id: recordId
+ }
+ };
+
+ this.do_action(modalAction, {
+ on_close: $.proxy(this._updateImage, this, recordId)
+ });
+ },
+
+ _slickRender: function(baseUrl, id) {
+ this._super(baseUrl, id);
+ this.$slick.find('img:last').data('record-id', id);
+ },
+
+ _updateImage: function (recordId) {
+ // SlickJS creates 'clones', so multiple elements need updated src
+ var $imgs = this.$slick.find('img').filter(function () {
+ return $(this).data('record-id') === recordId;
+ });
+ var $loaded = $imgs.filter('[src]');
+ var $notLoaded = $imgs.filter('[data-lazy]');
+
+ var imgUrl = $loaded.first().attr('src');
+ var imgUrlNew = imgUrl + "?unique=" + new Date().getTime();
+
+ $loaded.attr('src', imgUrlNew);
+ $notLoaded.attr('data-lazy', imgUrlNew);
+ }
+
+ });
+
+ core.form_widget_registry.add("slickroom", FieldSlickroomImages);
+
+ return {FieldSlickroomImages: FieldSlickroomImages};
+
+});
diff --git a/web_widget_slickroom/static/src/less/web_widget_slickroom.less b/web_widget_slickroom/static/src/less/web_widget_slickroom.less
new file mode 100644
index 00000000..23aff24d
--- /dev/null
+++ b/web_widget_slickroom/static/src/less/web_widget_slickroom.less
@@ -0,0 +1,6 @@
+/* Copyright 2017 LasLabs Inc.
+ * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */
+
+.o_form_editable .o_slickroom img {
+ cursor: pointer;
+}
diff --git a/web_widget_slickroom/static/tests/js/web_widget_slickroom.js b/web_widget_slickroom/static/tests/js/web_widget_slickroom.js
new file mode 100644
index 00000000..ee1bca1c
--- /dev/null
+++ b/web_widget_slickroom/static/tests/js/web_widget_slickroom.js
@@ -0,0 +1,179 @@
+/* Copyright 2017 LasLabs Inc.
+ * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). */
+
+odoo.define_section('web_widget_slickroom', ['web.core', 'web.form_common'], function (test) {
+ "use strict";
+
+ function appendWidget (core, formCommon, $fix) {
+ var fieldManager = new formCommon.DefaultFieldManager(null, {});
+ var node = {'attrs': {}};
+ var FieldSlickroomImages = core.form_widget_registry.get('slickroom');
+ var widget = new FieldSlickroomImages(fieldManager, node);
+ widget.appendTo($fix);
+ return widget;
+ }
+
+ function imgHTML (id, attr) {
+ return $(
+ ''
+ );
+ }
+
+ test('._openModal() should open a darkroom modal with provided options',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+ var recordId = 1;
+ widget.$slick.slick('slickAdd', imgHTML(recordId, 'src'));
+
+ var modalAction = {};
+ widget.do_action = function (action, options) {
+ modalAction = action;
+ };
+
+ var expectedAction = {
+ "type": "ir.actions.act_window",
+ "res_model": "darkroom.modal",
+ "name": "Darkroom",
+ "views": [[false, "form"]],
+ "target": "new",
+ "context": {
+ "active_field": widget.options.fieldName,
+ "active_model": widget.options.modelName,
+ "active_record_id": recordId
+ }
+ };
+
+ widget.$('img').click();
+
+ assert.deepEqual(modalAction, expectedAction);
+ }
+ );
+
+ test('._openModal() should open a darkroom modal with on_close action ' +
+ 'that calls ._updateImage()',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+ var recordId = 1;
+ widget.$slick.slick('slickAdd', imgHTML(recordId, 'src'));
+
+ var modalOptions = {};
+ widget.do_action = function (action, options) {
+ modalOptions = options;
+ };
+
+ var $img = widget.$('img');
+ $img.click();
+ modalOptions.on_close();
+
+ assert.notStrictEqual($img.attr('src').indexOf('?unique'), -1);
+ }
+ );
+
+ test('._slickRender() should add data-record-id to images',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+
+ var values = [1, 2, 3];
+
+ _.each(values, function(recordId) {
+ widget._slickRender('/web/image/ir.attachments/', recordId);
+ });
+
+ var slickImageIds = widget.$slick.find('img').map(function () {
+ return $(this).data('record-id');
+ }).get();
+
+ assert.deepEqual(slickImageIds, values);
+ }
+ );
+
+ test('._updateImage() should update source of matching/loaded slick images',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+ var imgId = 1;
+
+ for(var i = 0; i < 5; i++) {
+ widget.$slick.slick('slickAdd', imgHTML(imgId, 'src'));
+ }
+
+ widget._updateImage(imgId);
+
+ var $matches = widget.$slick.find(
+ '[data-record-id="' + imgId + '"]'
+ );
+ $matches.each(function () {
+ var newSrc = $(this).attr('src');
+ assert.notStrictEqual(newSrc.indexOf('?unique'), -1);
+ });
+ }
+ );
+
+ test('._updateImage() should update lazy data attribute of matching/unloaded slick images',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+ var imgId = 1;
+
+ for(var i = 0; i < 5; i++) {
+ widget.$slick.slick('slickAdd', imgHTML(imgId, 'data-lazy'));
+ }
+
+ widget._updateImage(1);
+
+ var $matches = widget.$slick.find(
+ '[data-record-id="' + imgId + '"]'
+ );
+ $matches.each(function () {
+ var newSrc = $(this).attr('data-lazy');
+ assert.notStrictEqual(newSrc.indexOf('?unique'), -1);
+ });
+ }
+ );
+
+ test('._updateImage() should not update source of non-matching/loaded slick images',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+ var imgId = 1;
+ var img2Id = 2;
+
+ widget.$slick.slick('slickAdd', imgHTML(imgId, 'src'));
+ widget.$slick.slick('slickAdd', imgHTML(img2Id, 'src'));
+
+ widget._updateImage(1);
+
+ var $notMatch = widget.$slick.find(
+ '[data-record-id="' + img2Id + '"]'
+ );
+ assert.strictEqual($notMatch.attr('src').indexOf('?unique'), -1);
+ }
+ );
+
+ test('._updateImage() should not update lazy data attribute of ' +
+ 'non-matching/unloaded slick images',
+ function (assert, core, formCommon) {
+ var $fix = $('#qunit-fixture');
+ var widget = appendWidget(core, formCommon, $fix);
+ var imgId = 1;
+ var img2Id = 2;
+
+ widget.$slick.slick('slickAdd', imgHTML(imgId, 'data-lazy'));
+ widget.$slick.slick('slickAdd', imgHTML(img2Id, 'data-lazy'));
+
+ widget._updateImage(1);
+
+ var $notMatch = widget.$slick.find(
+ '[data-record-id="' + img2Id + '"]'
+ );
+ assert.strictEqual(
+ $notMatch.attr('data-lazy').indexOf('?unique'), -1
+ );
+ }
+ );
+
+});
diff --git a/web_widget_slickroom/templates/assets.xml b/web_widget_slickroom/templates/assets.xml
new file mode 100644
index 00000000..a8608d69
--- /dev/null
+++ b/web_widget_slickroom/templates/assets.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web_widget_slickroom/tests/__init__.py b/web_widget_slickroom/tests/__init__.py
new file mode 100644
index 00000000..1091e3d5
--- /dev/null
+++ b/web_widget_slickroom/tests/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 LasLabs Inc.
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from . import test_ui
diff --git a/web_widget_slickroom/tests/test_ui.py b/web_widget_slickroom/tests/test_ui.py
new file mode 100644
index 00000000..4c440368
--- /dev/null
+++ b/web_widget_slickroom/tests/test_ui.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 LasLabs Inc.
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo.tests.common import HttpCase
+
+
+class UICase(HttpCase):
+ def test_ui_web(self):
+ """Test backend tests."""
+ self.phantom_js(
+ "/web/tests?debug=assets&module=web_widget_slickroom",
+ "",
+ login="admin",
+ )