diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website_slides/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_slides/static/src')
83 files changed, 7033 insertions, 0 deletions
diff --git a/addons/website_slides/static/src/components/activity/activity_tests.js b/addons/website_slides/static/src/components/activity/activity_tests.js new file mode 100644 index 00000000..d754428c --- /dev/null +++ b/addons/website_slides/static/src/components/activity/activity_tests.js @@ -0,0 +1,126 @@ +odoo.define('website_slides/static/src/tests/activity_tests.js', function (require) { +'use strict'; + +const components = { + Activity: require('mail/static/src/components/activity/activity.js'), +}; + +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('website_slides', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('activity', {}, function () { +QUnit.module('activity_tests.js', { + beforeEach() { + beforeEach(this); + + this.createActivityComponent = async activity => { + await createRootComponent(this, components.Activity, { + props: { activityLocalId: activity.localId }, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('grant course access', async function (assert) { + assert.expect(8); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'action_grant_access') { + assert.strictEqual(args.args.length, 1); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], 100); + assert.strictEqual(args.kwargs.partner_id, 5); + assert.step('access_grant'); + } + return this._super(...arguments); + }, + }); + const activity = this.env.models['mail.activity'].create({ + id: 100, + canWrite: true, + thread: [['insert', { + id: 100, + model: 'slide.channel', + }]], + requestingPartner: [['insert', { + id: 5, + displayName: "Pauvre pomme", + }]], + type: [['insert', { + id: 1, + displayName: "Access Request", + }]], + }); + await this.createActivityComponent(activity); + + assert.containsOnce(document.body, '.o_Activity', "should have activity component"); + assert.containsOnce(document.body, '.o_Activity_grantAccessButton', "should have grant access button"); + + document.querySelector('.o_Activity_grantAccessButton').click(); + assert.verifySteps(['access_grant'], "Grant button should trigger the right rpc call"); +}); + +QUnit.test('refuse course access', async function (assert) { + assert.expect(8); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'action_refuse_access') { + assert.strictEqual(args.args.length, 1); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], 100); + assert.strictEqual(args.kwargs.partner_id, 5); + assert.step('access_refuse'); + } + return this._super(...arguments); + }, + }); + const activity = this.env.models['mail.activity'].create({ + id: 100, + canWrite: true, + thread: [['insert', { + id: 100, + model: 'slide.channel', + }]], + requestingPartner: [['insert', { + id: 5, + displayName: "Pauvre pomme", + }]], + type: [['insert', { + id: 1, + displayName: "Access Request", + }]], + }); + await this.createActivityComponent(activity); + + assert.containsOnce(document.body, '.o_Activity', "should have activity component"); + assert.containsOnce(document.body, '.o_Activity_refuseAccessButton', "should have refuse access button"); + + document.querySelector('.o_Activity_refuseAccessButton').click(); + assert.verifySteps(['access_refuse'], "refuse button should trigger the right rpc call"); +}); + +}); +}); +}); + +}); diff --git a/addons/website_slides/static/src/img/banner_default.svg b/addons/website_slides/static/src/img/banner_default.svg new file mode 100644 index 00000000..1d8cbdaf --- /dev/null +++ b/addons/website_slides/static/src/img/banner_default.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="515"><defs><path id="a" d="M0 0L1280 0 1280 515 0 515z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#845978" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(-64 -902)"><path fill="#FFF" opacity=".105" d="M2443 1200.003L1425.898 1227.592 2428.994 1053.537 2410.807 959.354 1413.838 1188.559 2367.991 814.755 2332.04 726.066 1402.709 1149.229 2264.239 596.223 2212.162 516.325 1376.929 1117.888 2118.458 399.473 2052.002 331.594 1351.903 1085.917 1940.74 238.14 1862.693 184.686 1316.365 1067.012 1732.352 113.368 1645.54 76.631 1281.269 1047.257 1507.742 33.798 1415.612 14.927 1241.359 1043.705 1268.482 0 1174.518 0 1201.53 1039.145 1031.165 14.307 938.981 32.888 1163.316 1051.484 797.46 76.631 710.65 113.368 1124.834 1062.875 583.567 182.638 505.363 235.852 1094.152 1089.198 390.988 331.587 324.544 399.471 1062.838 1114.751 233.088 513.158 180.759 592.892 1044.335 1151.069 110.958 726.063 75.002 814.755 1025.021 1186.927 33.079 955.542 14.605 1049.674 1021.586 1227.721 0 1200.003 0 1295.992 1017.114 1268.403 14.001 1442.465 32.191 1536.639 1029.174 1307.438 75.002 1681.24 110.958 1769.932 1040.3 1346.768 178.761 1899.772 230.838 1979.679 1066.076 1378.114 324.544 2096.524 390.983 2164.406 1091.094 1410.09 502.255 2257.855 580.3 2311.319 1126.649 1428.964 710.641 2382.632 797.453 2419.374 1161.731 1448.736 935.256 2462.199 1027.383 2481.071 1201.646 1452.3 1174.518 2496 1268.482 2496 1241.475 1456.834 1411.837 2481.691 1504.019 2463.109 1279.686 1444.516 1645.54 2419.374 1732.352 2382.632 1318.176 1433.141 1859.435 2313.36 1937.649 2260.148 1348.874 1406.818 2052.009 2164.406 2118.458 2096.524 1380.157 1381.246 2209.92 1982.84 2262.241 1903.103 1398.639 1344.912 2332.047 1769.932 2367.991 1681.24 1417.991 1309.061 2409.923 1540.453 2428.386 1446.317 1421.502 1268.286 2443 1295.992 2443 1200.003"/><path fill="#845978" d="M64 1417L1344 1417 1344 1248 64 1248z"/><g transform="translate(892 1246)" fill="#573F51"><path d="M354.867 14.066L362.3 28 86.043 91.596 84.19 74.089z"/><path d="M248.749 6.147L253.38 18.931 28.168 71.1 30.604 58.435z"/><path d="M294.541 7.005L311.054 0 324.033 8.712 58.485 79.222 52.043 70.679z"/><path d="M172.008 16.138L209.092 6.884 213.083 12.127 0.734 61.158 0.891 56.487z"/></g><path fill="#573F51" d="M1111.493 1204.155c-.021.535-.06 1.102-.117 1.69-.356 3.73-1.408 8.343-3.014 11.624-2.639 5.398-4.017 11.736-3.912 16.782.105 5.044.703 9.657 1.787 13.639 1.082 3.979 2.8 6.916.164 9.283-1.315 1.183-2.172 1.28-3.133 1.056-.962-.225-2.029-.772-3.763-.88-3.468-.212-3.412.795-6.057 2.963-2.643 2.166-7.56 1.829-11.07.81-3.514-1.02-2.025-2.47-1.05-3.375 1.15-1.077 8.05-5.494 9.373-6.577 1.324-1.084 2.84-1.774 4.922-4.717 1.52-2.144 1.396-9.167 1.696-9.991.3-.824.118-26.067.225-27.286.004-.05.008-.118.011-.197.087-1.89.015-11.867-.166-15.148-.189-3.424-1.874-21.513-1.859-24.342.016-2.49-.223-10.77-.078-12.715.449-5.982 1.684-12.607 2.079-14.625-4.06-5.473-20.5-23.557-17.062-29.655 1.181-2.097 4.16-6.867 10.993-14.678 1.111-1.27 5.622-8.67 6.564-10.051 3.358-4.921 6.404-7.743 9.178-8.714.107-.066.203-.118.287-.154.1-.043.197-.088.292-.135.95-.765 2.077-1.477 2.687-1.967.56-.45.956-1.005 1.227-1.616a27.455 27.455 0 0 1-.397-.353c-3.066-1.651-6.327-5.703-7.704-11.32-1.135-4.632-1.509-9.206-.696-12.845.076-.969.141-1.94.123-2.693-.153-.132-.3-.27-.44-.414a4.451 4.451 0 0 1-1.253-2.742c-.086-1.08.41-1.714-.018-2.698-.936-2.12-.973-2.25.426-4.104 3.676-4.872 11.968-6.568 17.706-5.853 7.174.893 14.118 3.489 20.54 6.725 3.652 1.84 7.251 3.79 10.968 5.497 3.375 1.55 6.992 3.113 10.735 3.438 4.154.362 8.352-.28 12.49.392 1.773.287 4.26.443 5.61 1.78.247.247 2.205 3.21 1.81 3.409.094-.23-.967-1.403-1.137-1.597-.788-.9-1.703-1.352-2.893-1.395-2.524-.103-5.085.247-7.543.8.274.097 5.012 2.04 4.89 2.328-.026-.35-4.149-.697-4.446-.721a26.884 26.884 0 0 0-7.93.544c2.858.066 5.61.68 8.367 1.354-1.572.144-3.148.282-4.71.535-2.477.392-4.769 1.304-7.147 2.055 1.694.207 3.297-.054 4.943-.397 2.87-.585 5.825-.892 8.754-.665 2.58.206 5.575.828 7.278 2.95.675.842 1.216 1.858 1.36 2.94.042.318-.558 2.82-.905 2.474.293.189.435-1.687.407-1.808-.198-.881-.796-1.653-1.454-2.248-1.619-1.46-3.95-1.475-6.002-1.233-3.993.473-7.308 2.489-11.08 3.576a17.61 17.61 0 0 1-1.43.348c.369.618.762 1.225 1.192 1.814 3.905 5.34 10.308 2.235 15.31.52 5.26-1.803 12.645-2.675 17.262.952 1.685 1.324 2.916 3.496 3.463 5.545.46 1.726.58 3.588.226 5.343-.363 1.792-1.099 2.206-.193 3.969 1.318 2.584 3.388 4.936 5.84 6.492 1.452.92 3.156 1.594 4.903 1.485.444-.025 4.764-1.718 4.345-2.093.185.127-2.114 2.112-2.303 2.225-1.719 1.036-3.787 1.18-5.717.757-3.102-.674-6.367-2.076-9.164-3.541 1.545 2.146 2.88 4.442 4.263 6.694-1.37-1.242-2.724-2.503-4.113-3.723-1.867-1.632-3.826-3.274-6.185-4.136-2.203.983-4.22 2.36-6.147 3.799 3.977-.252 8.627.185 11.438 3.366 1.346 1.499 2.244 3.439 2.053 5.485-.036.397-1.543 4.917-1.985 4.59 1.155.42.785-5.933.5-6.49-1.256-2.448-3.925-3.62-6.564-3.715-4.79-.176-9.434 1.916-13.8 3.646-6.352 2.513-12.744 3.538-18.602 1.868 3.06 6.546 5.709 13.409 11.322 18.436 8.568 7.246 20.031 9.339 30.543 10.576 13.703 1.516 26.903 2.15 37.383 11.108 7.138 5.962 19.081 8.482 28.586 7.96-12.672.697-17.935 12.337-26.638 19.17-8.704 6.835-20.646 4.315-31.13 3.53-13.225-1.088-15.62 13.118-20.58 22.017-4.578 7.515-14.337 11.684-22.559 10.776-7.242-1.04-15.38-7.999-23.157-10.374.444 2.193.875 4.066.977 4.355.174.5 2.238 4.612 4.258 9.212l.151-.157.121.274c.48 1.099.92 2.14 1.308 3.094 1.502 3.69 2.926 9.864 1.196 18.319a212.782 212.782 0 0 1-.93 4.289c-1.299 5.803-2.16 9.635-1.35 17.602.223 2.225.77 4.009 1.255 5.586.917 2.99 1.712 5.576-.135 9.723-2.092 4.689-2.836 6.772-5.12 6.898-.625.032-1.266-.082-1.962-.356-2.96-1.16-4.164-8.26-2.866-12.488.598-1.944 1.118-3.414 1.575-4.71.498-1.413.892-2.528 1.21-3.745.581-2.25-2.86-21.918-3.369-24.276-.162-.752-.338-2.483-.558-4.675-.236-2.334-.528-5.24-.895-7.723l-.104-.697.247.29c-.31-2.02-.674-3.757-1.11-4.73-1.467-3.28-2.739-3-6.226-12.887-.306-.868-.602-1.722-.89-2.562-5.065 1.12-9.121 2.356-12.317 3.4-1.002.327-2.797 1.098-4.632 1.72.236 1.513.475 2.763.686 3.499.238.828.321 2.097.262 3.61z"/><path fill="#573F51" d="M1257.956 1185.867l.35-.214.211.39c.262.487.546.89.8 1.245.206.29.399.563.555.844l.061.109c4.155 7.467 5.06 15.14 5.086 20.264.037 7.473-2.373 17.109-3.969 23.485l-.231.919c.202 1.712.469 4.139.776 7.794.258 3.046.818 5.549 1.314 7.759 1.109 4.94 1.986 8.84-1.051 15.342-3.33 7.135-5.332 10.32-8.382 10.32h-.002c-.76 0-1.582-.191-2.584-.607-4.362-1.797-6.106-12.685-4.048-18.994 1.303-3.988 1.936-6.68 2.496-9.053.288-1.222.561-2.376.895-3.579.397-1.433.012-5.182-1.146-11.138-2.149-6.859-4.144-17.743-5.215-23.593-.146-.795-.275-1.5-.386-2.09-.369-1.975-.591-3.935-.808-5.828-.284-2.498-.554-4.857-1.118-6.696l-.311-1.014.518.295a8.435 8.435 0 0 0-.539-1.199c-1.739-3.133-2.525-1.848-3.936-6.913-1.073-3.85-2.386-7.25-4.824-11.594-.45 1.492-.972 2.944-1.467 4.45-.956 2.897-.981 5.362.018 8.193.372 1.055.661 2.155.879 3.283l.396-.17.092.522c.666 3.78.638 8.161-.082 13.025-1.258 8.369-5.06 15.851-9.181 23.415-.456.834-.956 1.661-1.483 2.538-2.069 3.434-4.209 6.985-3.53 10.831.24 1.357.664 2.715 1.117 4.154.708 2.268 1.443 4.614 1.383 6.941-.068 2.557-2.373 5.529-5.246 5.529-.293 0-.586-.031-.875-.094a32.806 32.806 0 0 1-1.978-.539c-1.333-.391-2.711-.799-4.04-.799-.814 0-1.54.15-2.22.461-.771.349-1.437.971-2.081 1.573-.201.191-.401.376-.606.556-1.635 1.443-3.043 2.228-4.708 2.621a15.96 15.96 0 0 1-3.687.409c-2.217 0-4.606-.39-7.102-1.161-.991-.309-3.054-.945-3.027-2.516.017-1.011.875-1.777 1.501-2.338l.329-.292c1.471-1.282 3.194-2.342 4.865-3.369.629-.389 1.286-.792 1.901-1.193 1.976-1.282 4.056-2.706 6.548-4.474 2.504-1.779 4.154-3.616 5.045-5.621.362-.812.772-1.619 1.167-2.398.774-1.529 1.576-3.109 2.029-4.762 1.025-3.75 1.81-15.59 1.818-15.697.195-5.522.458-11.423 1.046-17.151l.019-.185.15-.11c.1-.075.202-.149.305-.223.154-1.448.33-2.892.533-4.327.564-3.995 1.788-8.564-.075-12.392-1.48-3.041-1.218-6.035-1.48-9.355-.567-7.134-3.397-13.732-4.369-20.789a129.162 129.162 0 0 1-1.115-21.947c.093-2.66.242-5.345.652-7.965-.542-.86-1.107-1.598-1.697-2.116-2.767-2.43-16.811-11.084-19.283-15.707-2.477-4.623-.7-13.566.509-18.364 1.178-4.672 3.27-4.536 3.799-9.789.528-5.253 6.764-14.781 6.764-14.781s3.495-9.884 9.583-12.833c2.455-1.957 4.892-2.262 6.785-2.144.402-.1 4.41-1.094 8.661-2.19l-.045-.326c-2.84-.43-1.969-1.46-1.969-1.46s-.765.524-1.376.311c-1.565-.542-2.594-1.837-2.938-2.946-.345-1.108-.325-9.416-.89-10.669-.565-1.253-2.065-3.684-1.708-4.71.36-1.027 1.268-2.701 1.075-3.081-.194-.379-1.532-3.146-.991-4.745.196-.577.913-1.815 1.77-3.183-.175.187-.275.3-.275.3s.021-1.98 1.6-5.357c1.581-3.377 4.995-6.182 4.995-6.182l-.223 2.941s.534-2.55 3.965-3.455c3.43-.905 5.793.203 5.793.203l-1.948 1.433s3.144-1.532 6.542-1.134c3.399.396 6.636 3.149 6.636 3.149l-1.811.264s1.814 1.754 4.033 3.082c2.22 1.327 2.577 2.319 2.577 2.319s-1.825.38-2.071 2.489c-.247 2.109-.764 8.577-1.637 10.968-.663 1.813-1.795 3.57-2.304 4.313l.286.762c-1.129 2.61.276 4.743-.122 7.572a2.276 2.276 0 0 1-.34.877c13.797-2.203 30.236-1.437 38.839 1.84 16.726 6.432 26.376 23.159 45.675 23.802 19.301.644 38.601-12.223 58.543-7.076 5.79 1.287 9.649 4.504 13.511 8.363 5.146 4.503 10.291 9.006 16.084 12.223-12.225 4.504-21.873 2.573-23.805 18.657-1.285 13.509-1.93 26.375-17.369 29.592-13.511 3.217-27.663-3.86-39.887 4.504-7.72 5.146-7.077 14.797-11.58 21.872-4.502 9.651-10.936 16.083-21.871 14.797-12.226-1.287-21.873-9.65-34.098-9.007-2.497 0-4.996.607-7.493 1.819.9 8.704 1.49 16.902 1.077 20.069-.535 4.103-.024 6.368.676 7.846zm-47.861-77.259c1.04-3.92-.19-10.75-.803-14.726-.442-2.847-1.741-5.627-2.935-8.179-2.549 5.476-6.527 10.322-5.576 13.589 1.11 3.814 5.099 5.725 8.48 11.07.24-.634.691-1.929.691-1.929l.143.175z"/><g transform="translate(873 1139)"><path fill="#418ECA" d="M25.659 35.361c-3.172-2.336-6.91-.743-11.387 5.818-.707 1.039-4.095 6.596-4.929 7.549-5.133 5.867-7.37 9.449-8.257 11.024-2.707 4.801 10.995 19.498 13.189 22.801 2.192 3.301 1.362-6.213 1.362-6.213S7.703 63.739 7.749 63.136c.139-1.771 8.088-8.693 8.088-8.693s14.449-15.673 9.822-19.082"/><path fill="#1A1919" d="M12.857 51.647c-.474-1.979-.629-3.155.447-5.129.995-1.787 1.971-3.936 3.437-5.476 1.325-1.397 3.519-3.25 3.992-5.325 2.244-1.478 3.091-1.706 4.926-.356 4.627 3.409-9.822 19.082-9.822 19.082s-.308.237-1.477 1.316c-.536-1.429-1.226-2.819-1.503-4.112"/><path fill="#F1DCBC" d="M7.677 67.01c.265.114.596.29.919.272 0-.835-.057-1.567.078-2.329 2.082 3.634 6.963 11.387 6.963 11.387s.83 9.514-1.362 6.213c-1.503-2.263-8.412-9.878-11.739-15.992.605-.009 1.199-.003 1.761-.01 1.161.001 2.266.133 3.38.459"/><path fill="#2D2118" d="M67.078 66.508c-.148-.429-.288-.86-.423-1.295.256.374.301.868.423 1.295-.095-.271-.065-.227 0 0zm4.192-7.726c-.312 1.108-.841 2.211-1.687 3.014.877-3.611 1.518-6.704-.744-9.975 2.497 1.554 3.199 4.218 2.431 6.961-.235.835.232-.83 0 0zm-4.727 5.777c-1.107-1.913-1.362-4.535-2.186-6.615-.729-1.845-1.721-3.524-3.028-5.019 2.897.461 5.47 2.197 5.433 5.41-.026 2.201-.433 4.31-.067 6.509a19.071 19.071 0 0 0-.152-.285zM19.642 7.837c-5.382 8.241-4.964 16.814.79 24.746 1.901 4.55 4.693 8.136 8.709 11.016 2.418 1.72 2.927 4.571 3.602 7.322 1.965 8.011 8.398 11.746 16.182 13.188 3.469.641 7.258 1.163 10.237 3.186 1.639 1.113 2.871 2.924 2.693 4.983-.04.467-2.34 4.658-2.906 3.93.15.384 2.912-1.891 3.093-2.13.938-1.226 1.142-2.818.883-4.309-.52-3.145-3.305-5.278-5.936-6.704 1.801-.147 3.633-.22 5.427.033 1.158 1.491 1.749 3.317 2.285 5.1.399 1.331.757 2.674 1.133 4.01.018-1.985.084-3.978-.043-5.96 1.195 2.048 2.714 4.243 4.419 5.909 1.059 1.039 2.433 1.773 3.94 1.799.165.003 2.42-.342 2.354-.498.116.407-3.308-.239-3.6-.401-1.156-.627-1.972-1.736-2.529-2.901-.938-1.968-1.317-4.29-1.126-6.46.127-1.484.762-1.453 1.707-2.45.924-.975 1.591-2.208 1.986-3.489.47-1.523.555-3.395.009-4.91-1.49-4.15-6.537-6.541-10.605-7.492-3.865-.904-9.179-1.482-9.533-6.438-.265-3.698.89-7.276-.058-10.944-1.556-6.014-6.959-7.692-12.174-9.532-1.925-.677-3.754-1.549-5.235-2.977-1.689-1.636-1.027-5.038-1.737-7.147-.761-2.268-2.502-3.616-4.715-4.386-.508-.177-6.552-1.424-6.553-.103-.284.896-1.934 2.851-2.699 4.009-.848 1.298.856-1.295 0 0z"/><path fill="#F1DCBC" d="M31.302 24.242s.144 2.631 1.084 4.209c.939 1.576 5.973 4.308 5.973 4.308s-8.682 7.089-9.748 7.458c-1.065.368-3.41.033-4.292-.462-.88-.494-1.908-3.696-1.85-4.009.062-.315 1.136-1.925 1.472-2.874.336-.95.238-6.918.238-6.918l7.123-1.712"/><path fill="#C8A67A" d="M27.466 32.44c-1.221.265-2.352-.03-3.377-.598.155-1.998.09-5.888.09-5.888l7.123-1.712s.144 2.619 1.078 4.198c-1.212 1.75-2.845 3.554-4.914 4"/><path fill="#418ECA" d="M52.384 76.226c-.717-1.746-2.198-21.275-2.647-24.105-.475-3-1.764-5.256-2.042-6.095-.279-.838-.416-1.917-.386-2.772.032-.854-.726-6.617-3.544-9.424-1.527-1.519-4.439-1.695-5.757-1.7-1.32-.005-3.031.637-4.788 1.815-2.395 1.606-8.073 6.813-8.966 6.086-.893-.727-.697-5.476-1.035-6-.341-.524-1.003.133-1.837.489-.834.356-3.258 2.817-3.902 3.783-.39.583-.845 1.989-1.181 3.305-.949 1.267-1.916 2.872-2.225 3.489-.559 1.117-1.23 3.015-.252 5.289.979 2.274 2.491 4.363 3.246 5.406.079.111.179.241.288.38.183 1.025.346 2.078.509 3.211.377 2.617-.365 4.675-.652 6.475-.29 1.8-1.27 6.198-1.752 8.706 0 0 23.626 3.676 22.547 2.343-1.521-1.87-2.272-4.221-3.175-5.982-1.638-3.2-.891-6.997-.824-8.552.07-1.556 1.284-3.484 2.005-4.455.723-.971 2.262-5.428 2.692-6.073.429-.645 1.076 1.212 1.697 2.651 2.231 5.159 5.655 20.019 5.667 24.018.002.836.199 2.199.814 4.919.598 2.649 2.132 11.967 2.466 13.809.334 1.844-.053 1.865-.637 3.915-.586 2.05-.368 3.201-.459 4.368-.09 1.169.142 2.555.222 4.022.059 1.07-.04 1.492-.077 2.029.374-.035.749-.063 1.123-.098.18-.714-.036-1.908-.036-1.908l.035-.777c.035-.778.771-1.516.771-1.516s.843 2.667.877 3.286a4.56 4.56 0 0 1-.051.757c.57-.054 1.14-.113 1.71-.17.051-.312.085-.632.105-.994.077-1.4-.681-9.843-.62-11.552.062-1.711.832-16.989.917-18.235.088-1.247-.131-2.398-.846-4.143z"/><path fill="#F1DCBC" d="M13.275 114.357c.248 2.93.465 5.505.518 6.485.142 2.57.195 10.608.117 11.522-.08.915 10.276-.407 10.454-3.326.08-1.332.027-2.455-.175-3.157-.551-1.919-1.355-8.497-1.409-11.782-3.166.125-6.334.209-9.505.258zm32.218-.586c-.014-.246-.036-.633-.067-1.125-3.666.317-7.336.602-11.008.84.807 2.436 1.666 5.042 2.654 7.846 2.619 7.427 3.574 7.218 4.677 9.679.352.787.643 2.24.886 3.907.143.979 1.293 3.566 3.45 7.762 3.916-6.327 5.657-10.023 5.223-11.089a95.252 95.252 0 0 0-.948-2.244c-1.548-3.546-3.162-6.761-3.297-7.145-.165-.464-1.457-6.384-1.57-8.431z"/><path fill="#252A1D" d="M46.24 162.453c.456-1.76-2.155-16.589-2.526-18.303-.254-1.175-.565-5.746-1.09-9.302 1.91 2.246 3.142 4.89 3.142 4.89s.696-3.069 1.62-5.936c.631-1.954 2.083-3.652 2.94-4.533.334.764.669 1.546.982 2.322 1.111 2.729 2.197 7.29.892 13.669-1.306 6.379-2.464 9.117-1.716 16.484.49 4.86 2.943 6.733.853 11.422-2.09 4.687-3.008 5.666-5.125 4.834-2.117-.828-2.998-6.143-2.061-9.191.936-3.048 1.636-4.597 2.089-6.356m-44.51 6.389c.866-.809 6.047-4.125 7.04-4.938.995-.815 2.133-1.332 3.698-3.543 1.14-1.611 1.047-6.884 1.273-7.503.224-.62.089-19.579.169-20.494.003-.037.006-.088.008-.147a16.36 16.36 0 0 1 3.131-1.887c2.284-1.059 4.805-1.744 7.337-1.736-.117 2.953-.955 7.146-2.35 9.998-1.983 4.055-3.017 8.815-2.938 12.605.077 3.788.527 7.253 1.341 10.242.813 2.989 2.103 5.196.124 6.974-1.976 1.775-2.577.294-5.18.132-2.605-.16-2.563.598-4.549 2.226-1.984 1.625-5.679 1.373-8.314.608-2.639-.767-1.521-1.855-.79-2.537"/><path fill="#1A1919" d="M38.727 49.838c.449.402.692 1.411.731 2.504-.284-.531-.544-.81-.752-.497-.309.463-1.191 2.895-1.929 4.589.11-.501.173-1.069.172-1.725-.054-1.02-1.073-7.101 1.778-4.871"/><path fill="#F1DCBC" d="M21.655 8.25c4.388-1.947 12.618 2.256 12.52 10.33-.1 8.073-5.219 10.99-6.67 11.38-1.454.391-7.342-5.104-8.449-9.697-1.105-4.594-1.795-10.064 2.599-12.013"/><path fill="#50341D" d="M23.135.769c-1.631.621-2.748 2.046-2.998 3.769-.12.797-.689 1.004-1.064 1.724a3.338 3.338 0 0 0-.298 2.244c.571 2.631 3.126 4.794 5.193 6.362 2.318 1.757 4.762 3.332 7.077 5.095 1.198.912 2.495 1.752 3.588 2.787 1.112 1.048 1.803 2.418 2.41 3.802 1.484 3.374 2.887 6.494 5.852 8.851 1.346 1.07 2.862 1.8 4.174 2.894a12.895 12.895 0 0 0 4.708 2.483c2.835.813 5.748.852 8.477 2.144 1.402.665 2.881 1.604 3.329 3.18.181.641.253 1.37.027 2.011-.029.087-.869 1.224-.98.987.083.36 1.463-.994 1.563-1.212.341-.746.402-1.609.307-2.415-.236-2.029-1.894-3.62-3.454-4.78-1.773-1.313-3.776-2.296-5.836-3.069-1.184-.438-2.308-.912-3.303-1.72 1.813.471 3.635.805 5.368 1.544 1.095.463 2.153 1.004 3.211 1.539-1.486-1.529-2.993-3.018-4.785-4.198a20.193 20.193 0 0 1 5.264 2.815c.179.135 2.663 1.999 2.54 2.233.194-.134-2.047-3.261-2.183-3.432 1.785.628 3.555 1.427 5.121 2.501.738.501 1.14 1.155 1.283 2.041.031.192.239 1.36.086 1.47.331.032.268-2.636.208-2.891-.325-1.39-1.844-2.481-2.858-3.371-2.366-2.078-5.292-3.345-7.792-5.233-2.252-1.699-3.93-4.136-5.459-6.469-1.685-2.569-3.198-5.247-4.788-7.874-2.795-4.622-6.177-9.043-10.386-12.473C33.37 1.364 27.417-.864 23.135.769c-.943.36.948-.361 0 0"/><path fill="#343534" d="M11.419 107.536c0 3.597-.359 12.226-.359 12.226s9.709 2.878 19.777 3.238c10.069.359 17.98-3.598 19.418-4.316 1.439-.72 6.114-1.079 6.114-1.079S41.625 82.724 37.31 75.172c-4.315-7.552-21.935-.719-21.935-.719s-3.956 29.486-3.956 33.083z"/><path fill="#CBCCCB" d="M35.787 113.862L65.21 113.862 65.21 91.636 35.787 91.636z"/><path fill="#FFFFFE" d="M53.356 102.432a3.386 3.386 0 1 1-6.775 0 3.386 3.386 0 0 1 6.775 0"/><path fill="#F1DCBC" d="M56.411 114.063c-.102-.647-1.022-4.939-1.601-6.029-.579-1.09-2.213-5.415-2.316-6.301a6.177 6.177 0 0 0-.081-.502c-.072-1.187-.117-2.162-.1-2.627.056-1.573.712-14.603.884-17.671-1.449.638-2.606 1.769-2.948 3.462-.025.119-.162.146-.251.081-1.181-.862-2.301-1.782-3.46-2.641.098.475.213 1.002.346 1.598.598 2.649 2.132 11.967 2.466 13.809.334 1.844-.053 1.865-.637 3.915a10.41 10.41 0 0 0-.38 2.055l-.028.02s-1.124 5.755-1.159 6.744c-.034.988.103 2.794-.102 3.44-.204.647-.953 2.248-.953 2.248s1.226.408 1.669-.102c.442-.511 1.635-2.419 1.635-2.589 0-.17.238 2.112.238 2.112s.238 1.123.851.987c.614-.135.818-.987 1.397-.987.579 0 .238.681.783.681s.817-.919 1.26-.988c.443-.068.409.545 1.124.477.716-.068 1.465-.546 1.363-1.192"/></g><g transform="translate(925 1113)"><path fill="#907E63" d="M43.628 20.934s2.299 6.159 2.791 7.309c.493 1.149 1.971 4.106 2.3 4.27.328.164.083-.411 2.956-.247 2.874.165 2.793 3.203 2.793 3.203s-17.247 12.424-22.502 12.26c-5.257-.165-14.432-9.027-14.432-9.027s2.408-1.697 5.036-2.026c2.627-.328 7.508-1.207 7.508-1.207l-.82-5.995 14.37-8.54"/><path fill="#605442" d="M44.112 22.228l-.484-1.294-14.37 8.54.82 5.995.024.655c.666.174 1.467 0 2.132-.092.548-.075 1.096-.154 1.647-.16 3.391-.034 3.815-2.246 5.641-3.714.738-.595 1.789-1.177 2.504-1.841.698-.646 1.863-1.468 1.995-2.402.298-2.125-.756-3.727.091-5.687"/><path fill="#907E63" d="M23.137 12.53c-.406 1.201.598 3.28.743 3.564.146.285-.536 1.543-.805 2.314-.269.77.857 2.596 1.281 3.537.425.941.411 7.181.669 8.013.259.834 1.031 1.806 2.206 2.213.46.16 1.035-.233 1.035-.233s-.779.92 2.134 1.171 4.239.024 4.958-.784a13.006 13.006 0 0 1 3.252-2.652c1.532-.891 3.177-1.235 4.235-3.428 1.059-2.195.244-5.143.316-5.759.072-.615.908-3.193 1.026-4.962.117-1.77.029-9.982-4.898-10.736-4.928-.754-8.752-2.182-11.997 1.184-.347.361-3.747 5.358-4.155 6.558z"/><path fill="#252A1D" d="M10.417 216.905a11.96 11.96 0 0 0 2.741-.509c1.224-.385 2.236-1.049 3.383-2.22.142-.147.282-.296.423-.448.45-.487.914-.99 1.472-1.293a3.933 3.933 0 0 1 1.637-.468c.996-.072 2.05.157 3.07.377.499.11 1.016.22 1.512.296.22.031.441.039.661.023 2.152-.156 3.716-2.509 3.626-4.429-.082-1.747-.762-3.463-1.416-5.124-.417-1.051-.81-2.047-1.064-3.048-.719-2.846.689-5.624 2.051-8.308.347-.687.676-1.331.972-1.982 2.673-5.892 5.112-11.703 5.595-18.042.273-3.683.054-6.965-.652-9.759l-.097-.387-.354.183c-1.25.648-2.542 1.271-3.791 1.876-2.015.974-4.099 1.981-6.081 3.12-1.893 1.087-3.476 2.185-4.841 3.359l-.106.09-.003.14c-.127 4.324-.002 8.758.154 12.904.001.081.061 8.992-.502 11.858-.249 1.263-.763 2.49-1.259 3.679a37.83 37.83 0 0 0-.742 1.858c-.558 1.549-1.695 3.019-3.473 4.488a140.341 140.341 0 0 1-4.659 3.709c-.438.336-.908.671-1.358.999-1.195.86-2.429 1.749-3.46 2.788l-.002.001-.228.237c-.438.454-1.041 1.074-.997 1.832.066 1.178 1.646 1.541 2.406 1.72 1.912.439 3.723.601 5.382.48m48.342-17.409c-.061-.729-.12-1.391-.177-1.998-2.779.008-5.558.027-8.333.166.123 1.295.118 2.214-.023 2.725a69.888 69.888 0 0 0-.673 2.688c-.419 1.782-.896 3.802-1.874 6.799-1.546 4.737-.235 12.915 3.039 14.265.754.312 1.371.455 1.942.455h.001c2.291 0 3.795-2.391 6.296-7.75 2.28-4.883 1.622-7.814.789-11.523-.372-1.659-.794-3.54-.987-5.827"/><path fill="#1A1919" d="M43.173 6.685C41.498 1.614 35.196-.77 29.067 1.234c-6.128 2.004-9.714 7.787-8.059 12.795 5.94-5.694 14.013-8.316 22.165-7.344"/><path fill="#1A1919" d="M42.825 26.504c5.014-1.74 7.194-8.587 4.866-15.295C45.364 4.504 39.413.477 34.398 2.216l8.427 24.288"/><path fill="#907E63" d="M10.651 78.683a45.745 45.745 0 0 1 1.863-3.532c1.434 1.934 3.564 3.845 5.364 7.127 2.666 4.863 2.646 2.235 6.408 2.956 3.761.723 2.648 2.609 4.168 4.851 1.519 2.242 2.614 3.583 2.169 5.555-.446 1.982-2.352 3.947-6.213 4.788-2.96.642-4.507.99-6.024-.878-1.517-1.866-3.068-8.237-5.338-10.474l-2.663-10.294c.093.056.223-.01.266-.099"/><path fill="#343534" d="M19.27 83.782s-2.21 36.436-1.916 49.688c.269 12.047-2.613 44.024-2.613 44.024l-3.945 27.935 21.639.606s3.332-31.497 3.661-37.41c.329-5.914 3.075-39.73 3.075-39.73s4.058 45.376 4.387 47.675c.329 2.299 3.09 30.951 3.09 30.951l7.213 3.394 12.941-1.697s-1.899-43.82-2.885-49.732c-.985-5.914-2.495-21.418-3.48-27.332-.985-5.912-7.228-41.395-7.228-41.395l-1.971-3.614-31.968-3.363"/><path fill="#D89A13" d="M52.236 79.356c.742-4.081 5.936-21.521 5.936-21.521l12.653 14.101-15.621 17.439 3.339 8.163s24.155-19.666 25.639-23.005c1.485-3.34-15.62-33.394-18.959-36.734-3.34-3.34-10.532-5.159-13.8-5.362-3.269-.204-20.337 12.783-20.337 12.783l-2.339-9.926s-9.352.124-13.183 3.244c-2.459 2.001-7.112 10.393-9.338 15.958-2.227 5.566-9.018 15.987-4.937 20.811 4.082 4.824 13.789 17.201 13.789 17.201A639.882 639.882 0 0 0 18.968 87l-.34-5.241c-2.716-2.696-6.805-7.234-6.805-10.195 0-4.452 5.33-12.244 5.33-12.244l1.984 30.162 32.376-.816s-.019-5.228.723-9.31z"/><g transform="rotate(24 -140.5 137.11)"><path fill="#CBCCCB" d="M0.909 25.869L33.909 25.869 33.909 0.869 0.909 0.869z"/><path fill="#FFFFFE" d="M21.663 12.71a4 4 0 1 1-8.001 0 4 4 0 0 1 8 0"/></g><path fill="#907E63" d="M63.861 83.712l-7.227 3.596c-2.666 4.862-2.646 2.234-6.407 2.955-3.762.722-2.648 2.609-4.168 4.851-1.52 2.241-2.615 3.582-2.17 5.555.447 1.982 2.352 3.947 6.213 4.788 2.961.643 4.508.991 6.024-.878 1.517-1.866 3.068-8.238 5.338-10.474l2.663-10.294c-.093.057-.223-.01-.266-.099"/><path fill="#D89A13" d="M63.687 63.981l7.138 7.955-15.621 17.439 3.339 8.163s24.155-19.666 25.639-23.005c.76-1.711-3.358-10.434-8.005-18.985"/><path fill="#907E63" d="M39.404 18.504c.021 2.062.83 3.728 1.805 3.718.975-.011 1.748-1.692 1.727-3.755-.022-2.063-.829-3.728-1.804-3.717-.976.009-1.75 1.69-1.728 3.754"/><path fill="#583F07" d="M27.989 38.209L31.086 45.22 30.232 36.567 27.989 38.209"/><path fill="#886210" d="M29.74 33.119L26.456 34.268 24.54 42.862 30.232 36.567 29.74 33.119"/><path fill="#886210" d="M47.748 30.82L39.921 38.757 48.623 36.403 49.5 40.618 51.196 32.024 47.748 30.82"/></g></g></g></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/banner_default_all.svg b/addons/website_slides/static/src/img/banner_default_all.svg new file mode 100644 index 00000000..7d5c3a66 --- /dev/null +++ b/addons/website_slides/static/src/img/banner_default_all.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1280" height="515"><defs><path id="a" d="M0 0L1280 0 1280 515 0 515z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#845978" xlink:href="#a"/><g mask="url(#b)"><g transform="translate(-238 -978)"><path fill="#FFF" opacity=".101" d="M2560 1257.215L1494.187 1286.119 2545.323 1103.766 2526.265 1005.092 1481.549 1245.226 2481.399 853.6 2443.725 760.682 1469.888 1204.02 2372.678 624.649 2318.107 540.942 1442.872 1171.185 2219.915 418.519 2150.277 347.403 1416.649 1137.689 2033.686 249.494 1951.901 193.491 1379.408 1117.883 1815.318 118.773 1724.348 80.284 1342.631 1097.186 1579.95 35.41 1483.409 15.639 1300.81 1093.465 1329.232 0 1230.768 0 1259.073 1088.687 1080.55 14.989 983.951 34.456 1219.029 1101.615 835.652 80.284 744.685 118.773 1178.704 1113.549 611.515 191.345 529.566 247.096 1146.553 1141.127 409.713 347.396 340.088 418.516 1113.739 1167.899 244.251 537.623 189.416 621.159 1094.35 1205.947 116.272 760.679 78.594 853.6 1074.111 1243.515 34.664 1001.099 15.304 1099.718 1070.511 1286.254 0 1257.215 0 1357.78 1065.826 1328.876 14.672 1511.237 33.732 1609.9 1078.463 1369.772 78.594 1761.395 116.272 1854.316 1090.122 1410.977 187.322 1990.346 241.893 2074.063 1117.133 1443.818 340.088 2196.479 409.708 2267.597 1143.349 1477.318 526.309 2365.501 608.092 2421.514 1180.607 1497.092 744.675 2496.227 835.644 2534.721 1217.369 1517.806 980.047 2579.588 1076.586 2599.359 1259.195 1521.54 1230.768 2615 1329.232 2615 1300.932 1526.29 1479.453 2600.009 1576.049 2580.541 1340.973 1513.385 1724.348 2534.721 1815.318 2496.227 1381.305 1501.468 1948.487 2423.652 2030.447 2367.904 1413.474 1473.89 2150.284 2267.597 2219.915 2196.479 1446.256 1447.099 2315.757 2077.374 2370.584 1993.836 1465.623 1409.033 2443.733 1854.316 2481.399 1761.395 1485.901 1371.472 2525.339 1613.896 2544.686 1515.272 1489.581 1328.754 2560 1357.78 2560 1257.215"/><path fill="#845978" d="M238 1530L1649 1530 1649 1288 238 1288z"/><path fill="#62495B" d="M1184.337 1239.582c.847 2.898-.292 11.332-2.838 16.44-2.61 5.243-3.973 11.4-3.87 16.3.104 4.9.696 9.381 1.768 13.249.61 2.202 1.424 4.075 1.53 5.704.116 1.291-.188 2.43-1.356 3.457-2.603 2.295-3.393.38-6.824.172-3.432-.206-3.377.77-5.994 2.872-2.615 2.1-7.481 1.773-10.954.786-1.726-.491-2.233-1.085-2.187-1.666-.052-.639.623-1.273 1.146-1.75 1.139-1.047 7.964-5.337 9.272-6.39 1.31-1.052 2.808-1.722 4.869-4.58 1.502-2.083 1.38-8.905 1.677-9.706.296-.8.117-25.32.223-26.503.103-1.182.032-11.58-.154-14.905-.187-3.326-1.853-20.897-1.838-23.644.015-2.42-.22-10.46-.078-12.351.41-5.36 1.482-11.253 1.951-13.672-4.047-5.29-17.535-20.276-17.058-27.208-.057-.897.08-1.685.456-2.335 1.178-2.043 4.149-6.69 10.967-14.3 1.108-1.237 5.609-8.446 6.549-9.792 2.508-3.59 4.842-6.03 7.015-7.427-.027.117-.055.234-.085.351.4-.253.795-.47 1.184-.652.237-.186.453-.34.638-.455.473-.493 1.117-.989 1.75-1.443.481-1.24 1.321-2.015 2.509-2.517-3.11-1.568-6.466-5.624-7.866-11.291-.793-3.207-1.213-6.385-1.116-9.238a36.606 36.606 0 0 1-.004-1.584c.05-2.044-.037 2.043 0 0 .03-1.213.25-3.094.303-4.57a7.24 7.24 0 0 1-.374-.35 4.327 4.327 0 0 1-1.252-2.69c-.086-1.057.41-1.68-.018-2.645-.936-2.078-.973-2.206.426-4.024 3.676-4.777 11.967-6.44 17.705-5.739 7.173.876 14.116 3.421 20.537 6.594 3.653 1.805 7.251 3.718 10.968 5.39 3.375 1.521 6.992 3.053 10.734 3.371 4.154.355 8.352-.273 12.49.385 1.772.281 4.26.434 5.608 1.746.248.241 2.206 3.148 1.81 3.342.095-.226-.966-1.376-1.136-1.566-.788-.882-1.703-1.326-2.893-1.368-2.524-.101-5.084.242-7.542.785.274.095 5.011 2 4.888 2.282-.025-.343-4.147-.683-4.444-.707a27.4 27.4 0 0 0-7.93.534c2.858.064 5.61.666 8.367 1.327-1.572.141-3.148.277-4.711.525-2.476.384-4.768 1.278-7.145 2.015 1.693.203 3.296-.053 4.942-.39 2.869-.573 5.825-.874 8.754-.652 2.58.202 5.574.812 7.277 2.893.675.826 1.216 1.822 1.359 2.883.022.163-.131.914-.333 1.544 4.53-.863 9.602-.703 13.088 2.02 1.676 1.31 2.9 3.46 3.444 5.487.458 1.708.577 3.55.225 5.286-.36 1.774-1.093 2.183-.192 3.927 1.311 2.557 3.37 4.884 5.808 6.424 1.445.911 3.14 1.577 4.877 1.47.442-.026 4.739-1.7 4.322-2.072.184.126-2.103 2.09-2.29 2.202-1.71 1.025-3.767 1.167-5.687.749-3.085-.667-6.333-2.054-9.115-3.504 1.537 2.123 2.864 4.395 4.24 6.623-1.362-1.229-2.709-2.476-4.09-3.683-1.857-1.615-3.806-3.24-6.152-4.093-2.191.973-4.198 2.336-6.114 3.76 3.956-.25 8.58.182 11.377 3.33 1.338 1.483 2.231 3.402 2.041 5.427-.035.392-1.534 4.865-1.974 4.541 1.149.416.78-5.87.497-6.42-1.249-2.423-3.903-3.582-6.528-3.677-4.764-.174-9.383 1.896-13.727 3.607-6.39 2.517-12.823 3.525-18.704 1.79 2.999 6.302 5.64 12.88 11.16 17.713 8.531 7.055 19.945 9.093 30.411 10.297 13.644 1.476 26.787 2.094 37.221 10.814 7.107 5.805 18.999 8.258 28.462 7.75-12.617.678-17.857 12.01-26.522 18.664-8.667 6.653-20.557 4.2-30.995 3.437-13.168-1.06-15.554 12.77-20.491 21.434-4.558 7.316-14.275 11.375-22.461 10.491-7.107-.997-15.08-7.592-22.722-9.996.467 2.288.937 4.31 1.045 4.606.212.6 3.277 6.553 5.59 12.14 1.463 3.533 2.892 9.43 1.173 17.681-1.72 8.25-3.245 11.791-2.26 21.321.641 6.24 3.83 8.669 1.184 14.64-.103.294-.219.596-.349.907-1.936 4.633-2.625 6.691-4.74 6.816-.578.031-1.172-.081-1.816-.352-2.175-.91-3.326-5.513-3.108-9.464.019-1.23.178-2.415.49-3.412.688-2.203 1.28-3.8 1.771-5.177.302-.93.556-1.757.77-2.639.54-2.223-2.647-21.654-3.117-23.984-.05-.245-.1-.596-.154-1.034-.475-3.534-1.13-13.188-2.435-16.05-1.452-3.185-2.71-2.913-6.159-12.517-.37-1.031-.726-2.042-1.071-3.033-5.187 1.108-9.326 2.34-12.572 3.377-.905.288-2.457.932-4.1 1.502.263 1.753.538 3.221.777 4.037zm-11.709-65.5c1.087-5.093 2.296-10.76 2.52-12.129.378-2.328 1.356-4.99.86-8.373-.388-2.641-.758-4.978-1.322-7.285-.747.847-1.202 1.338-1.202 1.338s-9.15 7.925-10.484 10.847c.444 1.05 2.883 5.021 5.29 8.849 1.714 2.697 3.396 5.3 4.338 6.753zm61.355-68.14c.228.352.467.7.718 1.041 3.884 5.285 10.252 2.212 15.227.516 1.19-.406 2.49-.765 3.843-1.034.062-.433.087-.88.074-.937-.198-.864-.796-1.621-1.454-2.205-1.619-1.431-3.95-1.446-6.002-1.208-3.992.463-7.307 2.44-11.08 3.506-.437.124-.88.23-1.326.32z"/><path fill="#62495B" d="M1331.97 1238.626l.367-.223.221.408c.275.51.574.931.84 1.303.216.303.42.59.583.883l.064.114c4.362 7.816 5.312 15.846 5.339 21.21.039 7.821-2.491 17.906-4.167 24.58l-.242.962c.212 1.792.492 4.332.815 8.157.27 3.188.858 5.808 1.379 8.121 1.164 5.17 2.085 9.252-1.103 16.058-3.496 7.467-5.598 10.801-8.8 10.801h-.002c-.797 0-1.66-.2-2.712-.635-4.58-1.881-6.41-13.277-4.25-19.88 1.368-4.174 2.033-6.992 2.62-9.475.303-1.28.59-2.487.94-3.746.417-1.5.013-5.424-1.203-11.658-2.256-7.179-4.35-18.57-5.474-24.693-.154-.832-.289-1.57-.406-2.188-.387-2.067-.62-4.118-.848-6.1-.298-2.614-.581-5.083-1.173-7.008l-.327-1.061.544.309a8.815 8.815 0 0 0-.566-1.255c-1.825-3.28-2.65-1.935-4.132-7.236-1.126-4.03-2.505-7.587-5.064-12.134-.473 1.561-1.02 3.08-1.54 4.657-1.003 3.032-1.03 5.612.02 8.575.39 1.104.693 2.256.921 3.436l.417-.178.096.547c.7 3.956.67 8.541-.086 13.632-1.32 8.76-5.312 16.59-9.638 24.507-.478.873-1.003 1.739-1.557 2.657-2.172 3.594-4.418 7.31-3.705 11.336.252 1.42.697 2.841 1.172 4.348.744 2.373 1.515 4.829 1.452 7.264-.071 2.677-2.49 5.787-5.507 5.787-.307 0-.615-.032-.918-.098a34.52 34.52 0 0 1-2.077-.564c-1.4-.41-2.846-.837-4.24-.837-.855 0-1.618.157-2.331.483-.81.365-1.509 1.016-2.185 1.646-.21.2-.42.394-.636.582-1.716 1.51-3.194 2.332-4.942 2.743a16.802 16.802 0 0 1-3.87.429c-2.328 0-4.836-.409-7.456-1.216-1.04-.323-3.206-.989-3.178-2.633.018-1.058.919-1.86 1.576-2.447l.345-.306c1.544-1.341 3.353-2.45 5.107-3.526.66-.407 1.35-.829 1.996-1.248 2.074-1.342 4.258-2.833 6.874-4.683 2.628-1.862 4.36-3.785 5.296-5.883.38-.85.81-1.695 1.225-2.51.812-1.6 1.654-3.254 2.13-4.984 1.076-3.925 1.9-16.317 1.908-16.43.205-5.779.481-11.955 1.098-17.95l.02-.194.158-.115c.106-.078.212-.156.32-.233.162-1.516.347-3.027.56-4.53.592-4.18 1.876-8.963-.08-12.97-1.553-3.182-1.278-6.316-1.553-9.79-.595-7.467-3.566-14.373-4.586-21.76a134.789 134.789 0 0 1-1.17-22.97c.096-2.784.253-5.595.683-8.337-.568-.9-1.161-1.672-1.78-2.214-2.905-2.543-17.648-11.6-20.243-16.44-2.6-4.838-.735-14.198.534-19.22 1.237-4.89 3.433-4.748 3.988-10.246.554-5.498 7.1-15.47 7.1-15.47s3.67-10.345 10.06-13.431c2.578-2.049 5.136-2.368 7.123-2.245.422-.104 4.63-1.145 9.092-2.29l-.047-.343c-2.981-.45-2.067-1.528-2.067-1.528s-.803.548-1.445.326c-1.642-.568-2.723-1.923-3.084-3.084-.362-1.16-.341-9.855-.934-11.167-.593-1.311-2.168-3.855-1.793-4.93.378-1.074 1.331-2.826 1.128-3.224-.203-.397-1.608-3.293-1.04-4.966.206-.605.958-1.9 1.858-3.332-.183.196-.289.314-.289.314s.023-2.072 1.68-5.607c1.66-3.534 5.244-6.47 5.244-6.47l-.234 3.078s.56-2.669 4.162-3.616c3.6-.947 6.081.213 6.081.213l-2.045 1.5s3.3-1.604 6.868-1.187c3.568.414 6.966 3.295 6.966 3.295l-1.901.277s1.904 1.836 4.233 3.226c2.33 1.388 2.706 2.427 2.706 2.427s-1.916.397-2.174 2.605c-.26 2.207-.802 8.977-1.719 11.48-.695 1.896-1.884 3.735-2.418 4.514l.3.797c-1.185 2.732.29 4.964-.128 7.925a2.378 2.378 0 0 1-.358.918c14.484-2.305 31.741-1.504 40.772 1.926 17.559 6.732 27.689 24.24 47.948 24.912 20.262.674 40.522-12.793 61.456-7.406 6.078 1.347 10.13 4.714 14.184 8.753 5.402 4.713 10.803 9.426 16.884 12.793-12.833 4.714-22.961 2.693-24.99 19.527-1.348 14.14-2.026 27.606-18.233 30.973-14.183 3.367-29.04-4.04-41.872 4.714-8.104 5.386-7.429 15.487-12.156 22.892-4.726 10.1-11.48 16.833-22.96 15.487-12.834-1.347-22.96-10.1-35.794-9.427-2.62 0-5.245.635-7.866 1.903.945 9.11 1.565 17.69 1.13 21.006-.56 4.294-.024 6.665.71 8.211zm-50.243-80.861c1.092-4.104-.2-11.253-.843-15.414-.464-2.979-1.828-5.89-3.08-8.56-2.677 5.731-6.853 10.803-5.855 14.223 1.166 3.992 5.353 5.992 8.902 11.585.251-.663.726-2.018.726-2.018l.15.184z"/></g></g></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/banner_heroes_default.svg b/addons/website_slides/static/src/img/banner_heroes_default.svg new file mode 100644 index 00000000..9f353625 --- /dev/null +++ b/addons/website_slides/static/src/img/banner_heroes_default.svg @@ -0,0 +1 @@ +<svg height="749" viewBox="0 0 1920 749" width="1920" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="m0 0h1920v749h-1920z"/><mask id="b" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#a"/></mask></defs><g fill="none" fill-rule="evenodd"><use fill="#36576b" fill-rule="nonzero" xlink:href="#a"/><path d="m3341 938.466-1537.519 41.694 1516.347-263.036-27.493-142.331-1507.085 346.379 1442.362-564.899-54.347-134.029-1404.837 639.492 1302.345-835.712-78.722-120.744-1262.595 909.092 1120.945-1085.68-100.459-102.581-1058.316 1139.946 890.125-1281.175-117.982-80.781-825.865 1333.387 628.834-1441.164-131.231-55.518-550.657 1466.828 342.352-1531.557-139.269-28.519-263.413 1554.707 41.001-1577.265h-142.042l40.832 1570.374-257.534-1548.753-139.351 28.08 339.119 1539.321-553.052-1473.216-131.227 55.518 626.107 1434.912-818.215-1330.23-118.218 80.418 890.052 1289.592-1062.949-1144.916-100.44 102.588 1116.053 1080.944-1254.306-909.138-79.103 120.495 1305.438 843.526-1410.953-642.275-54.355 134.033 1436.113 562.432-1499.485-349.672-27.928 142.252 1522.219 269.069-1544.296-41.888v145.061l1537.537-41.694-1516.372 263.046 27.497 142.318 1507.106-346.373-1442.391 564.896 54.355 134.033 1404.855-639.493-1302.36 835.709 78.722 120.758 1262.602-909.096-1120.948 1085.674 100.433 102.584 1058.334-1139.935-890.128 1281.157 117.978 80.795 825.897-1333.43-628.866 1441.2 131.231 55.525 550.668-1466.845-342.356 1531.564 139.266 28.519 263.427-1554.697-41.008 1577.258h142.042l-40.825-1570.407 257.53 1548.783 139.348-28.081-339.116-1539.317 553.049 1473.223 131.231-55.525-626.097-1434.887 818.204 1330.202 118.233-80.415-890.031-1289.567 1062.906 1144.88 100.448-102.584-1116.064-1080.941 1254.323 909.139 79.093-120.499-1305.478-843.548 1411.001 642.297 54.336-134.033-1436.084-562.443 1499.471 349.683 27.91-142.26-1522.072-269.043 1544.163 41.87z" fill="#fff" mask="url(#b)" opacity=".043"/></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/channel-documentation-default.jpg b/addons/website_slides/static/src/img/channel-documentation-default.jpg Binary files differnew file mode 100644 index 00000000..ffa294e9 --- /dev/null +++ b/addons/website_slides/static/src/img/channel-documentation-default.jpg diff --git a/addons/website_slides/static/src/img/channel-documentation-layout.png b/addons/website_slides/static/src/img/channel-documentation-layout.png Binary files differnew file mode 100644 index 00000000..6967033a --- /dev/null +++ b/addons/website_slides/static/src/img/channel-documentation-layout.png diff --git a/addons/website_slides/static/src/img/channel-training-default.jpg b/addons/website_slides/static/src/img/channel-training-default.jpg Binary files differnew file mode 100644 index 00000000..6d1bf293 --- /dev/null +++ b/addons/website_slides/static/src/img/channel-training-default.jpg diff --git a/addons/website_slides/static/src/img/channel-training-layout.png b/addons/website_slides/static/src/img/channel-training-layout.png Binary files differnew file mode 100644 index 00000000..cb79d38e --- /dev/null +++ b/addons/website_slides/static/src/img/channel-training-layout.png diff --git a/addons/website_slides/static/src/img/channel_demo_furniture.jpg b/addons/website_slides/static/src/img/channel_demo_furniture.jpg Binary files differnew file mode 100644 index 00000000..f16ff623 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_furniture.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_furniture_2.jpg b/addons/website_slides/static/src/img/channel_demo_furniture_2.jpg Binary files differnew file mode 100644 index 00000000..375036f7 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_furniture_2.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_furniture_3.jpg b/addons/website_slides/static/src/img/channel_demo_furniture_3.jpg Binary files differnew file mode 100644 index 00000000..fe51b1cc --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_furniture_3.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_gardening.jpg b/addons/website_slides/static/src/img/channel_demo_gardening.jpg Binary files differnew file mode 100644 index 00000000..2446a667 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_gardening.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg b/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg Binary files differnew file mode 100644 index 00000000..f5899039 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_gardening_2.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_gardening_3.jpg b/addons/website_slides/static/src/img/channel_demo_gardening_3.jpg Binary files differnew file mode 100644 index 00000000..199d7785 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_gardening_3.jpg diff --git a/addons/website_slides/static/src/img/channel_demo_tree_1.jpg b/addons/website_slides/static/src/img/channel_demo_tree_1.jpg Binary files differnew file mode 100644 index 00000000..af533083 --- /dev/null +++ b/addons/website_slides/static/src/img/channel_demo_tree_1.jpg diff --git a/addons/website_slides/static/src/img/document.png b/addons/website_slides/static/src/img/document.png Binary files differnew file mode 100644 index 00000000..68ebd707 --- /dev/null +++ b/addons/website_slides/static/src/img/document.png diff --git a/addons/website_slides/static/src/img/onboarding-quiz.png b/addons/website_slides/static/src/img/onboarding-quiz.png Binary files differnew file mode 100644 index 00000000..1ab746f3 --- /dev/null +++ b/addons/website_slides/static/src/img/onboarding-quiz.png diff --git a/addons/website_slides/static/src/img/presentation.pdf b/addons/website_slides/static/src/img/presentation.pdf Binary files differnew file mode 100644 index 00000000..774c2ea7 --- /dev/null +++ b/addons/website_slides/static/src/img/presentation.pdf diff --git a/addons/website_slides/static/src/img/quiz_modal_success.svg b/addons/website_slides/static/src/img/quiz_modal_success.svg new file mode 100644 index 00000000..7bb62ea1 --- /dev/null +++ b/addons/website_slides/static/src/img/quiz_modal_success.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="377" height="503"><defs><filter id="a" width="218.6%" height="210.3%" x="-59.3%" y="-55.1%" filterUnits="objectBoundingBox"><feOffset in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="110"/><feColorMatrix in="shadowBlurOuter1" result="shadowMatrixOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.126174607 0"/><feMerge><feMergeNode in="shadowMatrixOuter1"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g fill="none" fill-rule="evenodd" filter="url(#a)" transform="scale(-1 1) rotate(10 -170.596 -2124.816)"><path fill="#DD9E63" d="M200.228 136.773l-.97 5.966 3.6 8.464-13.154-2.359c-2.309 4.07 1.061 8.372 10.107 12.904 3.324 1.295 5.078 1.156 5.262-.416l2.078 4.44.415.415 18.523 10.223 9.554 2.914 30.74 8.88-5.507-18.27c-3.046.832-8.4 1.063-16.062.693l14.124-7.353-.696.752.835-.613 1.246-.833 4.016-2.914-23.955-42.317-9.14-9.296-4.292-3.053-30.602 24.42 3.878 7.353z"/><path fill="#17252E" d="M342.943 366.508l-.139-.14c0 .092-.046.186-.14.279l-.976-.14h-.976l.28-.278L333 364c7.53 15.138 12.41 28.836 14.641 41.096 3.08 16.986 4.64 32.406 4.683 46.262l12.708-2.261-6.75-38.29-15.339-43.882v-.417zM108 100.758c-.647-.648-1.551-1.84-2.105-2.394-15.432-15.187-39.272-33.43-71.52-54.727V31.969l-3.189-15.278L16.494.022C9.287-.533 3.788 9.374 0 29.746c8.87 9.816 16.217 17.317 22.038 22.502 11.459 25.743 25.202 49.275 41.004 69.368 17.28 9.26 32.644 3.938 44.01-16.803"/><path fill="#321714" d="M116 148l14-1.82-5.142-12.18c.094 5.133-2.857 9.8-8.858 14"/><path fill="#5E2A1C" d="M344 289L363 301 357.396 289.061z"/><path fill="#17252E" d="M322.087 175.43a298.543 298.543 0 0 0-9.12-14.039 5.557 5.557 0 0 0-.828-1.391c.184 1.112.46 2.179.829 3.198a223.398 223.398 0 0 1 1.935 10.842c1.013 7.875 1.243 15.567.69 23.073-.276 3.15-.69 6.301-1.244 9.452a2.631 2.631 0 0 0-.138.834V208.094c9.12 40.495 8.382 70.982-2.211 91.462 4.422-2.688 8.291-5.607 11.606-8.757 9.95-8.897 16.489 15.66 19.62 73.67 1.197-3.8 2.212-7.553 3.04-11.26.46.092.967.231 1.52.417 23.303 7.91 44.755 13.517 64.357 16.82l-8.682-49.268c-12.076-6.763-24.923-13.368-38.542-19.815l-19.481-12.093 12.02.416 38.134-2.78 19.758 5.56c6.448-6.486 9.533-12.973 9.257-19.459l-13.954-12.232-11.883 8.201c-14.554-8.248-26.943-14.734-37.167-19.46-5.16-2.317-9.764-4.216-13.817-5.699a46.205 46.205 0 0 0-5.25-1.529c1.105-13.437-1.98-28.494-9.258-45.175-3.04-6.95-6.77-14.177-11.191-21.684z"/><path fill="#8C633C" d="M244.453 170.75c7.31.398 13.64.315 16.547-.578l-1.538-7.023-.793.595.661-.744-14.877 7.75z"/><path fill="#A92121" d="M264.154 160.259l-5.154 3.75c10.533-.759 21.498-.795 29.741.725l2.748.57c7.42 1.615 14.42 4.35 19.825 7.864.915-3.23 1.726-7.252 2.275-10.862-.51-.818-1.121-1.772-1.49-2.846l-3.573-.854c-4.763-1.236-10.442-1.758-17.037-1.568h-2.748c-7.053.474-15.52 1.321-24.587 3.22zm-60.15 11.836l7.495 9.23 1.103.28c5.707 1.302 11.713 3.23 17.788 5.277 2.669.836 5.337 1.768 8.007 2.791 1.749.745 3.506 1.512 5.347 2.257 6.074 2.605 12.212 5.3 18.563 8.558L267 203l-31.093-24.578-9.525-2.931-18.086-9.073-.414-.418-7.606.512-.276-.14v.698l4.004 4.746v.279zM156.477 221c-14.296 20.087-29.667 62.398-46.114 126.935-.934 5.486-1.854 10.52-3.162 15.077 4.111-12.926 13.295-30.181 27.404-51.756 14.577-23.62 22.526-52.454 24.395-86.49L156.477 221z"/><path fill="#6E1414" d="M360.542 411.783c11.138-9.89 24.829-10.215 41.072-.978 6.243 3.059 12.472 4.846 18.745 6.193l-7.12-40.378c-15.604-2.143-39.017-5.35-70.239-9.62l15.315 44.084 2.227.7zm-101.268-247.55l1.587 6.787 5.177 16.603-30.038-8.49 30.464 23.778c5.042 2.546 10.172 5.998 15.4 9.089 2.428-1.727 5.329-4.394 7.57-6.304a63.564 63.564 0 0 1 2.801-2.319c2.522-2.362 4.762-4.727 6.723-7.091 6.07-7.182 10.52-15.695 13.042-24.15-5.51-3.365-12.046-5.82-19.61-7.365-.933-.182-1.867-.363-2.8-.546-8.404-1.454-17.991-1.486-28.729-.76l-1.587.767zm-100.6 61.223c-1.857 33.97-10.02 62.742-24.495 86.317-14.01 21.533-23.056 38.75-27.138 51.65v.278c-.093.093-.186.28-.28.558a2.65 2.65 0 0 0-.138.834c-.185.28-.279.604-.279.975v.14c-3.99 13.922-9.834 22.832-17.535 26.73L87 395.863c6.587-.28 13.035-.743 19.344-1.394 17.722-1.855 34.7-5.475 50.938-10.859 2.876-.741 5.66-1.392 8.35-1.948 2.876-.465 5.614-.928 8.21-1.393 11.32-1.763 20.831-1.95 28.532-.557v.975l.278-.975c1.114.28 2.273.557 3.479.836 8.072 2.134 13.731 6.31 16.98 12.53 1.669 3.248 4.174 5.801 7.515 7.657 2.319 1.299 5.01 2.367 8.072 3.202 1.113.186 2.273.418 3.478.696.558 0 1.207.046 1.95.139.649.093 1.391.232 2.225.417 4.547.28 9.79.14 15.727-.417h.836c11.226-1.206 20.226-.65 27 1.672.741.185 1.437.37 2.086.556l-13.639-25.617c-29.844-51.171-45.756-78.088-47.735-80.748-12.805-16.985-31.732-38.054-56.783-63.207-2.69-2.598-5.428-5.243-8.211-7.935a249.14 249.14 0 0 0-8.628-8.493l1.67 4.456z"/><path fill="#1A1B19" d="M201.018 137.549L197.107 130.186 227.973 105.73 232.302 108.787 233 107.537 221.827 96.838 207.861 86 183 120.597 188.168 127.962 183.42 136.437 190.403 149.638 203.671 152 200.04 143.523 201.018 137.549"/><path fill="#BE242B" d="M88.809 392.938c-1.866 1.273-4.675 2.335-6.809 3.062 1.422-.091 3.578-.137 5-.137"/><path fill="#2A4454" d="M291.425 408.806c6.33 16.017 16.288 32.033 29.255 48.05l32.599-5.735c-.067-13.728-1.613-28.99-4.637-45.788-2.208-12.226-7.042-25.886-14.5-40.983-43.822-26.488-63.385-20.792-56.941 17.088L290.734 407c.277.648.506 1.25.691 1.806zm-125.91-267.711c-12.163-6.753-25.257-11.886-39.278-15.4-2.508-6.937-8.59-15.168-18.247-24.695a64.781 64.781 0 0 0-1.81 3.607c-11.422 20.717-25.897 26.102-43.262 16.852 7.428 9.342 15.45 18.48 23.9 26.71a179.683 179.683 0 0 0 8.775 8.186 377.875 377.875 0 0 0 9.333 7.908l1.253 1.387c18.387 15.17 37.37 29.475 58.543 39.279a218.728 218.728 0 0 0 8.218 3.469c18.015 7.492 37.752 11.267 57.624 14.967l8.079 1.388c1.764.368 3.575.647 5.432.831 1.021.185 1.997.324 2.925.416l-34.266-44.534-1.113-.277-7.522-8.88v-.277l-4.04-4.717v-.693c-8.263-7.954-17.039-14.845-26.325-20.671a105.437 105.437 0 0 0-8.218-4.856z"/><path fill="#192A35" d="M130 146.18L116 148c6-4.2 8.952-8.867 8.858-14L130 146.18"/><path fill="#36576B" d="M156.23 221.4c2.962 2.785 6.15 5.811 8.926 8.689 2.778 2.691 5.509 5.337 8.193 7.935 24.997 25.157 43.882 46.229 56.658 63.217a270.764 270.764 0 0 1 8.054 11.417c.864 1.361 14.056 24.475 39.576 69.342-6.48-37.966 12.313-43.675 56.38-17.127l7.083 2.228.693.279.973.139c.093-.093.138-.187.138-.278l.14.139v.417l70.341 9.637-1.334-7.557c-19.514-3.647-40.868-9.586-64.172-17.47a12.468 12.468 0 0 0-1.527-.417 136.644 136.644 0 0 1-3.055 11.278c-3.148-58.11-9.002-82.357-19-73.446-3.333 3.157-7.221 6.08-11.666 8.773 10.647-20.515 11.388-51.055 2.223-91.621v-.14-.556c0-.278.045-.557.138-.835.557-3.157.973-6.313 1.25-9.469.556-7.52.325-15.224-.693-23.113-.557-3.621-1.219-6.936-1.96-10.555-.555 3.527-1.281 6.563-2.206 9.719-2.5 8.633-6.759 16.616-12.776 23.95-1.945 2.413-4.166 4.827-6.666 7.24a63.466 63.466 0 0 0-2.777 2.367 101.299 101.299 0 0 1-6.943 5.57 260.687 260.687 0 0 0-15.276-8.633c-1.573-.836-3.148-1.67-4.721-2.507-6.388-3.249-12.637-6.173-18.747-8.771a323.35 323.35 0 0 1-5.416-2.229 137.827 137.827 0 0 0-8.054-2.784c-6.11-2.042-12.035-3.714-17.775-5.013l34.161 44.696a36.576 36.576 0 0 1-2.916-.418 50.824 50.824 0 0 1-5.416-.834c-2.685-.465-5.37-.93-8.054-1.393-19.812-3.714-38.697-9.329-56.658-16.848a217.273 217.273 0 0 1-8.193-3.482c-21.107-9.84-40.825-22.37-59.156-37.595l50.23 56.06z"/></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/slide_demo_furniture.jpg b/addons/website_slides/static/src/img/slide_demo_furniture.jpg Binary files differnew file mode 100644 index 00000000..57012055 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_furniture.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_gardening.jpg b/addons/website_slides/static/src/img/slide_demo_gardening.jpg Binary files differnew file mode 100644 index 00000000..3993e10b --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_gardening.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_gardening_1.jpg b/addons/website_slides/static/src/img/slide_demo_gardening_1.jpg Binary files differnew file mode 100644 index 00000000..75dc371a --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_gardening_1.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_gardening_2.jpg b/addons/website_slides/static/src/img/slide_demo_gardening_2.jpg Binary files differnew file mode 100644 index 00000000..69af99a7 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_gardening_2.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_owl.jpg b/addons/website_slides/static/src/img/slide_demo_owl.jpg Binary files differnew file mode 100644 index 00000000..05e7ac6d --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_owl.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_thumb_5WMqwTnZ-qs.jpg b/addons/website_slides/static/src/img/slide_demo_thumb_5WMqwTnZ-qs.jpg Binary files differnew file mode 100644 index 00000000..b76505fb --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_thumb_5WMqwTnZ-qs.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_thumb_PYr1rK8pS30.jpg b/addons/website_slides/static/src/img/slide_demo_thumb_PYr1rK8pS30.jpg Binary files differnew file mode 100644 index 00000000..84370ee3 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_thumb_PYr1rK8pS30.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_thumb_QYmgrw0PgLU.jpg b/addons/website_slides/static/src/img/slide_demo_thumb_QYmgrw0PgLU.jpg Binary files differnew file mode 100644 index 00000000..737a0e1d --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_thumb_QYmgrw0PgLU.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_thumb_ebBez6bcSEc.jpg b/addons/website_slides/static/src/img/slide_demo_thumb_ebBez6bcSEc.jpg Binary files differnew file mode 100644 index 00000000..f534e522 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_thumb_ebBez6bcSEc.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_thumb_l0JZ25VvbwE.jpg b/addons/website_slides/static/src/img/slide_demo_thumb_l0JZ25VvbwE.jpg Binary files differnew file mode 100644 index 00000000..1afde8b5 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_thumb_l0JZ25VvbwE.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_thumb_ptjeDDoURL8.jpg b/addons/website_slides/static/src/img/slide_demo_thumb_ptjeDDoURL8.jpg Binary files differnew file mode 100644 index 00000000..0bf060b5 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_thumb_ptjeDDoURL8.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_img_1.jpg b/addons/website_slides/static/src/img/slide_demo_tree_img_1.jpg Binary files differnew file mode 100644 index 00000000..693c05b6 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_img_1.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_img_2.jpg b/addons/website_slides/static/src/img/slide_demo_tree_img_2.jpg Binary files differnew file mode 100644 index 00000000..91490c1b --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_img_2.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_img_3.jpg b/addons/website_slides/static/src/img/slide_demo_tree_img_3.jpg Binary files differnew file mode 100644 index 00000000..4169af23 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_img_3.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_infographic_1.jpg b/addons/website_slides/static/src/img/slide_demo_tree_infographic_1.jpg Binary files differnew file mode 100644 index 00000000..00373853 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_infographic_1.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_infographic_2.jpg b/addons/website_slides/static/src/img/slide_demo_tree_infographic_2.jpg Binary files differnew file mode 100644 index 00000000..76071b34 --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_infographic_2.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_tree_infographic_3.jpg b/addons/website_slides/static/src/img/slide_demo_tree_infographic_3.jpg Binary files differnew file mode 100644 index 00000000..9fa0e98a --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_tree_infographic_3.jpg diff --git a/addons/website_slides/static/src/img/slide_demo_wood_infographic_1.jpg b/addons/website_slides/static/src/img/slide_demo_wood_infographic_1.jpg Binary files differnew file mode 100644 index 00000000..71985e2e --- /dev/null +++ b/addons/website_slides/static/src/img/slide_demo_wood_infographic_1.jpg diff --git a/addons/website_slides/static/src/img/standard_badge_bronze.svg b/addons/website_slides/static/src/img/standard_badge_bronze.svg new file mode 100644 index 00000000..a94491e8 --- /dev/null +++ b/addons/website_slides/static/src/img/standard_badge_bronze.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300"><g fill="none"><circle cx="150" cy="150" r="150" fill="#FFF"/><path fill="#C37933" d="M150 300C67.157 300 0 232.843 0 150S67.157 0 150 0s150 67.157 150 150-67.157 150-150 150zm0-9.375c77.665 0 140.625-62.96 140.625-140.625 0-77.665-62.96-140.625-140.625-140.625C72.335 9.375 9.375 72.335 9.375 150c0 77.665 62.96 140.625 140.625 140.625zm0-14.063C80.101 276.563 23.437 219.9 23.437 150S80.102 23.437 150 23.437 276.563 80.102 276.563 150 219.899 276.563 150 276.563zm56.757-126.57l12.882-12.602c1.867-1.743 2.49-3.922 1.867-6.536-.747-2.551-2.365-4.139-4.854-4.76l-17.55-4.482 4.947-17.365c.747-2.552.156-4.73-1.773-6.535-1.805-1.93-3.983-2.52-6.535-1.774l-17.363 4.948-4.48-17.551c-.623-2.552-2.21-4.14-4.761-4.761-2.552-.685-4.73-.094-6.535 1.773L150 93.324l-12.602-12.977c-1.805-1.93-3.983-2.52-6.535-1.773-2.551.622-4.138 2.209-4.76 4.76l-4.481 17.552-17.363-4.948c-2.552-.747-4.73-.155-6.535 1.774-1.929 1.805-2.52 3.983-1.773 6.535l4.947 17.365-17.55 4.481c-2.489.623-4.107 2.21-4.854 4.761-.622 2.614 0 4.793 1.867 6.536l12.882 12.603-12.882 12.603c-1.867 1.743-2.49 3.921-1.867 6.535.747 2.552 2.365 4.14 4.854 4.762l17.55 4.481-4.947 17.365c-.747 2.552-.156 4.73 1.773 6.535 1.805 1.93 3.983 2.52 6.535 1.774l17.363-4.948 4.48 17.551c.623 2.552 2.21 4.17 4.761 4.855 2.614.622 4.792 0 6.535-1.867L150 206.755l12.602 12.884c1.245 1.369 2.832 2.053 4.761 2.053.436 0 1.027-.062 1.774-.186 2.551-.747 4.138-2.365 4.76-4.855l4.481-17.551 17.363 4.948c2.552.747 4.73.155 6.535-1.774 1.929-1.805 2.52-3.983 1.773-6.535l-4.947-17.365 17.55-4.481c2.489-.623 4.107-2.21 4.854-4.762.622-2.614 0-4.792-1.867-6.535l-12.882-12.603z"/></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/standard_badge_gold.svg b/addons/website_slides/static/src/img/standard_badge_gold.svg new file mode 100644 index 00000000..423f63e6 --- /dev/null +++ b/addons/website_slides/static/src/img/standard_badge_gold.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300"><g fill="none"><circle cx="150" cy="150" r="150" fill="#FFF"/><path fill="#E2BE00" d="M150 300C67.157 300 0 232.843 0 150S67.157 0 150 0s150 67.157 150 150-67.157 150-150 150zm0-9.375c77.665 0 140.625-62.96 140.625-140.625 0-77.665-62.96-140.625-140.625-140.625C72.335 9.375 9.375 72.335 9.375 150c0 77.665 62.96 140.625 140.625 140.625zm0-14.063C80.101 276.563 23.437 219.9 23.437 150S80.102 23.437 150 23.437 276.563 80.102 276.563 150 219.899 276.563 150 276.563zm56.757-126.57l12.882-12.602c1.867-1.743 2.49-3.922 1.867-6.536-.747-2.551-2.365-4.139-4.854-4.76l-17.55-4.482 4.947-17.365c.747-2.552.156-4.73-1.773-6.535-1.805-1.93-3.983-2.52-6.535-1.774l-17.363 4.948-4.48-17.551c-.623-2.552-2.21-4.14-4.761-4.761-2.552-.685-4.73-.094-6.535 1.773L150 93.324l-12.602-12.977c-1.805-1.93-3.983-2.52-6.535-1.773-2.551.622-4.138 2.209-4.76 4.76l-4.481 17.552-17.363-4.948c-2.552-.747-4.73-.155-6.535 1.774-1.929 1.805-2.52 3.983-1.773 6.535l4.947 17.365-17.55 4.481c-2.489.623-4.107 2.21-4.854 4.761-.622 2.614 0 4.793 1.867 6.536l12.882 12.603-12.882 12.603c-1.867 1.743-2.49 3.921-1.867 6.535.747 2.552 2.365 4.14 4.854 4.762l17.55 4.481-4.947 17.365c-.747 2.552-.156 4.73 1.773 6.535 1.805 1.93 3.983 2.52 6.535 1.774l17.363-4.948 4.48 17.551c.623 2.552 2.21 4.17 4.761 4.855 2.614.622 4.792 0 6.535-1.867L150 206.755l12.602 12.884c1.245 1.369 2.832 2.053 4.761 2.053.436 0 1.027-.062 1.774-.186 2.551-.747 4.138-2.365 4.76-4.855l4.481-17.551 17.363 4.948c2.552.747 4.73.155 6.535-1.774 1.929-1.805 2.52-3.983 1.773-6.535l-4.947-17.365 17.55-4.481c2.489-.623 4.107-2.21 4.854-4.762.622-2.614 0-4.792-1.867-6.535l-12.882-12.603z"/></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/img/standard_badge_silver.svg b/addons/website_slides/static/src/img/standard_badge_silver.svg new file mode 100644 index 00000000..dd16d4f2 --- /dev/null +++ b/addons/website_slides/static/src/img/standard_badge_silver.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300"><g fill="none"><circle cx="150" cy="150" r="150" fill="#FFF"/><path fill="#838997" d="M150 300C67.157 300 0 232.843 0 150S67.157 0 150 0s150 67.157 150 150-67.157 150-150 150zm0-9.375c77.665 0 140.625-62.96 140.625-140.625 0-77.665-62.96-140.625-140.625-140.625C72.335 9.375 9.375 72.335 9.375 150c0 77.665 62.96 140.625 140.625 140.625zm0-14.063C80.101 276.563 23.437 219.9 23.437 150S80.102 23.437 150 23.437 276.563 80.102 276.563 150 219.899 276.563 150 276.563zm56.757-126.57l12.882-12.602c1.867-1.743 2.49-3.922 1.867-6.536-.747-2.551-2.365-4.139-4.854-4.76l-17.55-4.482 4.947-17.365c.747-2.552.156-4.73-1.773-6.535-1.805-1.93-3.983-2.52-6.535-1.774l-17.363 4.948-4.48-17.551c-.623-2.552-2.21-4.14-4.761-4.761-2.552-.685-4.73-.094-6.535 1.773L150 93.324l-12.602-12.977c-1.805-1.93-3.983-2.52-6.535-1.773-2.551.622-4.138 2.209-4.76 4.76l-4.481 17.552-17.363-4.948c-2.552-.747-4.73-.155-6.535 1.774-1.929 1.805-2.52 3.983-1.773 6.535l4.947 17.365-17.55 4.481c-2.489.623-4.107 2.21-4.854 4.761-.622 2.614 0 4.793 1.867 6.536l12.882 12.603-12.882 12.603c-1.867 1.743-2.49 3.921-1.867 6.535.747 2.552 2.365 4.14 4.854 4.762l17.55 4.481-4.947 17.365c-.747 2.552-.156 4.73 1.773 6.535 1.805 1.93 3.983 2.52 6.535 1.774l17.363-4.948 4.48 17.551c.623 2.552 2.21 4.17 4.761 4.855 2.614.622 4.792 0 6.535-1.867L150 206.755l12.602 12.884c1.245 1.369 2.832 2.053 4.761 2.053.436 0 1.027-.062 1.774-.186 2.551-.747 4.138-2.365 4.76-4.855l4.481-17.551 17.363 4.948c2.552.747 4.73.155 6.535-1.774 1.929-1.805 2.52-3.983 1.773-6.535l-4.947-17.365 17.55-4.481c2.489-.623 4.107-2.21 4.854-4.762.622-2.614 0-4.792-1.867-6.535l-12.882-12.603z"/></g></svg>
\ No newline at end of file diff --git a/addons/website_slides/static/src/js/activity.js b/addons/website_slides/static/src/js/activity.js new file mode 100644 index 00000000..ac0ae3cd --- /dev/null +++ b/addons/website_slides/static/src/js/activity.js @@ -0,0 +1,87 @@ +odoo.define('website_slides.Activity', function (require) { +"use strict"; + +var field_registry = require('web.field_registry'); + +require('mail.Activity'); + +var KanbanActivity = field_registry.get('kanban_activity'); + +function applyInclude(Activity) { + Activity.include({ + events: _.extend({}, Activity.prototype.events, { + 'click .o_activity_action_grant_access': '_onGrantAccess', + 'click .o_activity_action_refuse_access': '_onRefuseAccess', + }), + + _onGrantAccess: function (event) { + var self = this; + var partnerId = $(event.currentTarget).data('partner-id'); + this._rpc({ + model: 'slide.channel', + method: 'action_grant_access', + args: [this.res_id, partnerId], + }).then(function (result) { + self.trigger_up('reload'); + }); + }, + + _onRefuseAccess: function (event) { + var self = this; + var partnerId = $(event.currentTarget).data('partner-id'); + this._rpc({ + model: 'slide.channel', + method: 'action_refuse_access', + args: [this.res_id, partnerId], + }).then(function () { + self.trigger_up('reload'); + }); + }, + }); +} + +applyInclude(KanbanActivity); + +}); + +odoo.define('website_slides/static/src/components/activity/activity.js', function (require) { +'use strict'; + +const components = { + Activity: require('mail/static/src/components/activity/activity.js'), +}; +const { patch } = require('web.utils'); + +patch(components.Activity, 'website_slides/static/src/components/activity/activity.js', { + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + async _onGrantAccess(ev) { + await this.env.services.rpc({ + model: 'slide.channel', + method: 'action_grant_access', + args: [[this.activity.thread.id]], + kwargs: { partner_id: this.activity.requestingPartner.id }, + }); + this.trigger('reload'); + }, + /** + * @private + */ + async _onRefuseAccess(ev) { + await this.env.services.rpc({ + model: 'slide.channel', + method: 'action_refuse_access', + args: [[this.activity.thread.id]], + kwargs: { partner_id: this.activity.requestingPartner.id }, + }); + this.trigger('reload'); + }, +}); + +}); diff --git a/addons/website_slides/static/src/js/rating_field_backend.js b/addons/website_slides/static/src/js/rating_field_backend.js new file mode 100644 index 00000000..12a6d8b6 --- /dev/null +++ b/addons/website_slides/static/src/js/rating_field_backend.js @@ -0,0 +1,42 @@ +odoo.define('website_slides.ratingField', function (require) { +"use strict"; + +var basicFields = require('web.basic_fields'); +var fieldRegistry = require('web.field_registry'); + +var core = require('web.core'); + +var QWeb = core.qweb; + +var FieldFloatRating = basicFields.FieldFloat.extend({ + xmlDependencies: !basicFields.FieldFloat.prototype.xmlDependencies ? + ['/portal_rating/static/src/xml/portal_tools.xml'] : basicFields.FieldFloat.prototype.xmlDependencies.concat( + ['/portal_rating/static/src/xml/portal_tools.xml'] + ), + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _render: function () { + var self = this; + + return Promise.resolve(this._super()).then(function () { + self.$el.html(QWeb.render('portal_rating.rating_stars_static', { + 'val': self.value / 2, + 'inline_mode': true + })); + }); + }, +}); + +fieldRegistry.add('field_float_rating', FieldFloatRating); + +return { + FieldFloatRating: FieldFloatRating, +}; + +}); diff --git a/addons/website_slides/static/src/js/slide_category_one2many.js b/addons/website_slides/static/src/js/slide_category_one2many.js new file mode 100644 index 00000000..adb84d60 --- /dev/null +++ b/addons/website_slides/static/src/js/slide_category_one2many.js @@ -0,0 +1,182 @@ +odoo.define('survey.slide_category_one2many', function (require){ +"use strict"; + +var Context = require('web.Context'); +var FieldOne2Many = require('web.relational_fields').FieldOne2Many; +var FieldRegistry = require('web.field_registry'); +var ListRenderer = require('web.ListRenderer'); +var config = require('web.config'); + +var SectionListRenderer = ListRenderer.extend({ + init: function (parent, state, params) { + this.sectionFieldName = "is_category"; + this._super.apply(this, arguments); + }, + _checkIfRecordIsSection: function (id){ + var record = this._findRecordById(id); + return record && record.data[this.sectionFieldName]; + }, + _findRecordById: function (id){ + return _.find(this.state.data, function (record){ + return record.id === id; + }); + }, + /** + * Allows to hide specific field in case the record is a section + * and, in this case, makes the 'title' field take the space of all the other + * fields + * @private + * @override + * @param {*} record + * @param {*} node + * @param {*} index + * @param {*} options + */ + _renderBodyCell: function (record, node, index, options){ + var $cell = this._super.apply(this, arguments); + + var isSection = record.data[this.sectionFieldName]; + + if (isSection){ + if (node.attrs.widget === "handle"){ + return $cell; + } else if (node.attrs.name === "name"){ + var nbrColumns = this._getNumberOfCols(); + if (this.handleField){ + nbrColumns--; + } + if (this.addTrashIcon){ + nbrColumns--; + } + $cell.attr('colspan', nbrColumns); + } else { + $cell.removeClass('o_invisible_modifier'); + return $cell.addClass('o_hidden'); + } + } + return $cell; + }, + /** + * Adds specific classes to rows that are sections + * to apply custom css on them + * @private + * @override + * @param {*} record + * @param {*} index + */ + _renderRow: function (record, index){ + var $row = this._super.apply(this, arguments); + if (record.data[this.sectionFieldName]) { + $row.addClass("o_is_section"); + } + return $row; + }, + /** + * Adding this class after the view is rendered allows + * us to limit the custom css scope to this particular case + * and no other + * @private + * @override + */ + _renderView: function (){ + var def = this._super.apply(this, arguments); + var self = this; + return def.then(function () { + self.$('table.o_list_table').addClass('o_section_list_view'); + }); + }, + // Handlers + /** + * Overriden to allow different behaviours depending on + * the row the user clicked on. + * If the row is a section: edit inline + * else use a normal modal + * @private + * @override + * @param {*} ev + */ + _onRowClicked: function (ev){ + var parent = this.getParent(); + var recordId = $(ev.currentTarget).data('id'); + var is_section = this._checkIfRecordIsSection(recordId); + if (is_section && parent.mode === "edit"){ + this.editable = "bottom"; + } else { + this.editable = null; + } + this._super.apply(this, arguments); + }, + /** + * Overriden to allow different behaviours depending on + * the cell the user clicked on. + * If the cell is part of a section: edit inline + * else use a normal edit modal + * @private + * @override + * @param {*} ev + */ + _onCellClick: function (ev){ + var parent = this.getParent(); + var recordId = $(ev.currentTarget.parentElement).data('id'); + var is_section = this._checkIfRecordIsSection(recordId); + if (is_section && parent.mode === "edit"){ + this.editable = "bottom"; + } else { + this.editable = null; + this.unselectRow(); + } + this._super.apply(this, arguments); + }, + /** + * In this case, navigating in the list caused issues. + * For example, editing a section then pressing enter would trigger + * the inline edition of the next element in the list. Which is not desired + * if the next element ends up being a question and not a section + * @override + * @param {*} ev + */ + _onNavigationMove: function (ev){ + this.unselectRow(); + }, +}); + +var SectionFieldOne2Many = FieldOne2Many.extend({ + init: function (parent, name, record, options){ + this._super.apply(this, arguments); + this.sectionFieldName = "is_category"; + this.rendered = false; + }, + /** + * Overriden to use our custom renderer + * @private + * @override + */ + _getRenderer: function (){ + if (this.view.arch.tag === 'tree'){ + return SectionListRenderer; + } + return this._super.apply(this, arguments); + }, + /** + * Overriden to allow different behaviours depending on + * the object we want to add. Adding a section would be done inline + * while adding a question would render a modal. + * @private + * @override + * @param {*} ev + */ + _onAddRecord: function (ev) { + this.editable = null; + if (!config.device.isMobile){ + var context_str = ev.data.context && ev.data.context[0]; + var context = new Context(context_str).eval(); + if (context['default_' + this.sectionFieldName]){ + this.editable = "bottom"; + } + } + this._super.apply(this, arguments); + }, +}); + +FieldRegistry.add('slide_category_one2many', SectionFieldOne2Many); +});
\ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides.js b/addons/website_slides/static/src/js/slides.js new file mode 100644 index 00000000..4f081013 --- /dev/null +++ b/addons/website_slides/static/src/js/slides.js @@ -0,0 +1,124 @@ +odoo.define('website_slides.slides', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var time = require('web.time'); + +publicWidget.registry.websiteSlides = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + /** + * @override + * @param {Object} parent + */ + start: function (parent) { + var defs = [this._super.apply(this, arguments)]; + + _.each($("timeago.timeago"), function (el) { + var datetime = $(el).attr('datetime'); + var datetimeObj = time.str_to_datetime(datetime); + // if presentation 7 days, 24 hours, 60 min, 60 second, 1000 millis old(one week) + // then return fix formate string else timeago + var displayStr = ''; + if (datetimeObj && new Date().getTime() - datetimeObj.getTime() > 7 * 24 * 60 * 60 * 1000) { + displayStr = moment(datetimeObj).format('ll'); + } else { + displayStr = moment(datetimeObj).fromNow(); + } + $(el).text(displayStr); + }); + + return Promise.all(defs); + }, +}); + +return publicWidget.registry.websiteSlides; + +}); + +//============================================================================== + +odoo.define('website_slides.slides_embed', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +require('website_slides.slides'); + +var SlideSocialEmbed = publicWidget.Widget.extend({ + events: { + 'change input': '_onChangePage', + }, + /** + * @constructor + * @param {Object} parent + * @param {Number} maxPage + */ + init: function (parent, maxPage) { + this._super.apply(this, arguments); + this.max_page = maxPage || false; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Number} page + */ + _updateEmbeddedCode: function (page) { + var $embedInput = this.$('.slide_embed_code'); + var newCode = $embedInput.val().replace(/(page=).*?([^\d]+)/, '$1' + page + '$2'); + $embedInput.val(newCode); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} ev + */ + _onChangePage: function (ev) { + ev.preventDefault(); + var input = this.$('input'); + var page = parseInt(input.val()); + if (this.max_page && !(page > 0 && page <= this.max_page)) { + page = 1; + } + this._updateEmbeddedCode(page); + }, +}); + +publicWidget.registry.websiteSlidesEmbed = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + /** + * @override + * @param {Object} parent + */ + start: function (parent) { + var defs = [this._super.apply(this, arguments)]; + $('iframe.o_wslides_iframe_viewer').on('ready', this._onIframeViewerReady.bind(this)); + return Promise.all(defs); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onIframeViewerReady: function (ev) { + // TODO : make it work. For now, once the iframe is loaded, the value of #page_count is + // still now set (the pdf is still loading) + var $iframe = $(ev.currentTarget); + var maxPage = $iframe.contents().find('#page_count').val(); + new SlideSocialEmbed(this, maxPage).attachTo($('.oe_slide_js_embed_code_widget')); + }, +}); + +}); diff --git a/addons/website_slides/static/src/js/slides_category_add.js b/addons/website_slides/static/src/js/slides_category_add.js new file mode 100644 index 00000000..e11ba2a4 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_category_add.js @@ -0,0 +1,84 @@ +odoo.define('website_slides.category.add', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var Dialog = require('web.Dialog'); +var core = require('web.core'); +var _t = core._t; + +var CategoryAddDialog = Dialog.extend({ + template: 'slides.category.add', + + /** + * @override + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t('Add a section'), + size: 'medium', + buttons: [{ + text: _t('Save'), + classes: 'btn-primary', + click: this._onClickFormSubmit.bind(this) + }, { + text: _t('Discard'), + close: true + }] + }); + + this.channelId = options.channelId; + this._super(parent, options); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _formValidate: function ($form) { + $form.addClass('was-validated'); + return $form[0].checkValidity(); + }, + + _onClickFormSubmit: function (ev) { + var $form = this.$('#slide_category_add_form'); + if (this._formValidate($form)) { + $form.submit(); + } + }, +}); + +publicWidget.registry.websiteSlidesCategoryAdd = publicWidget.Widget.extend({ + selector: '.o_wslides_js_slide_section_add', + xmlDependencies: ['/website_slides/static/src/xml/slide_management.xml'], + events: { + 'click': '_onAddSectionClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function (channelId) { + new CategoryAddDialog(this, {channelId: channelId}).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onAddSectionClick: function (ev) { + ev.preventDefault(); + this._openDialog($(ev.currentTarget).attr('channel_id')); + }, +}); + +return { + categoryAddDialog: CategoryAddDialog, + websiteSlidesCategoryAdd: publicWidget.registry.websiteSlidesCategoryAdd +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_course_enroll_email.js b/addons/website_slides/static/src/js/slides_course_enroll_email.js new file mode 100644 index 00000000..a9f5f799 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_enroll_email.js @@ -0,0 +1,83 @@ +odoo.define('website_slides.course.enroll', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); +var _t = core._t; + +var SlideEnrollDialog = Dialog.extend({ + template: 'slide.course.join.request', + + init: function (parent, options, modalOptions) { + modalOptions = _.defaults(modalOptions || {}, { + title: _t('Request Access.'), + size: 'medium', + buttons: [{ + text: _t('Yes'), + classes: 'btn-primary', + click: this._onSendRequest.bind(this) + }, { + text: _t('Cancel'), + close: true + }] + }); + this.$element = options.$element; + this.channelId = options.channelId; + this._super(parent, modalOptions); + }, + + _onSendRequest: function () { + var self = this; + this._rpc({ + model: 'slide.channel', + method: 'action_request_access', + args: [self.channelId] + }).then(function (result) { + if (result.error) { + self.$element.replaceWith('<div class="alert alert-danger" role="alert"><strong>' + result.error + '</strong></div>'); + } else if (result.done) { + self.$element.replaceWith('<div class="alert alert-success" role="alert"><strong>' + _t('Request sent !') + '</strong></div>'); + } else { + self.$element.replaceWith('<div class="alert alert-danger" role="alert"><strong>' + _t('Unknown error, try again.') + '</strong></div>'); + } + self.close(); + }); + } + +}); + +publicWidget.registry.websiteSlidesEnroll = publicWidget.Widget.extend({ + selector: '.o_wslides_js_channel_enroll', + xmlDependencies: ['/website_slides/static/src/xml/slide_course_join.xml'], + events: { + 'click': '_onSendRequestClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function (channelId) { + new SlideEnrollDialog(this, { + channelId: channelId, + $element: this.$el + }).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onSendRequestClick: function (ev) { + ev.preventDefault(); + this._openDialog($(ev.currentTarget).data('channelId')); + } +}); + +return { + slideEnrollDialog: SlideEnrollDialog, + websiteSlidesEnroll: publicWidget.registry.websiteSlidesEnroll +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js new file mode 100644 index 00000000..d9aa707c --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -0,0 +1,762 @@ +var onYouTubeIframeAPIReady = undefined; + +odoo.define('website_slides.fullscreen', function (require) { + 'use strict'; + + var publicWidget = require('web.public.widget'); + var core = require('web.core'); + var config = require('web.config'); + var QWeb = core.qweb; + var _t = core._t; + + var session = require('web.session'); + + var Quiz = require('website_slides.quiz').Quiz; + + var Dialog = require('web.Dialog'); + + require('website_slides.course.join.widget'); + + /** + * Helper: Get the slide dict matching the given criteria + * + * @private + * @param {Array<Object>} slideList List of dict reprensenting a slide + * @param {Object} matcher (see https://underscorejs.org/#matcher) + */ + var findSlide = function (slideList, matcher) { + var slideMatch = _.matcher(matcher); + return _.find(slideList, slideMatch); + }; + + /** + * This widget is responsible of display Youtube Player + * + * The widget will trigger an event `change_slide` when the video is at + * its end, and `slide_completed` when the player is at 30 sec before the + * end of the video (30 sec before is considered as completed). + */ + var VideoPlayer = publicWidget.Widget.extend({ + template: 'website.slides.fullscreen.video', + youtubeUrl: 'https://www.youtube.com/iframe_api', + + init: function (parent, slide) { + this.slide = slide; + return this._super.apply(this, arguments); + }, + start: function (){ + var self = this; + return Promise.all([this._super.apply(this, arguments), this._loadYoutubeAPI()]).then(function() { + self._setupYoutubePlayer(); + }); + }, + _loadYoutubeAPI: function () { + var self = this; + var prom = new Promise(function (resolve, reject) { + if ($(document).find('script[src="' + self.youtubeUrl + '"]').length === 0) { + var $youtubeElement = $('<script/>', {src: self.youtubeUrl}); + $(document.head).append($youtubeElement); + + // function called when the Youtube asset is loaded + // see https://developers.google.com/youtube/iframe_api_reference#Requirements + onYouTubeIframeAPIReady = function () { + resolve(); + }; + } else { + resolve(); + } + }); + return prom; + }, + /** + * Links the youtube api to the iframe present in the template + * + * @private + */ + _setupYoutubePlayer: function (){ + this.player = new YT.Player('youtube-player' + this.slide.id, { + playerVars: { + 'autoplay': 1, + 'origin': window.location.origin + }, + events: { + 'onStateChange': this._onPlayerStateChange.bind(this) + } + }); + }, + /** + * Specific method of the youtube api. + * Whenever the player starts playing/pausing/buffering/..., a setinterval is created. + * This setinterval is used to check te user's progress in the video. + * Once the user reaches a particular time in the video (30s before end), the slide will be considered as completed + * if the video doesn't have a mini-quiz. + * This method also allows to automatically go to the next slide (or the quiz associated to the current + * video) once the video is over + * + * @private + * @param {*} event + */ + _onPlayerStateChange: function (event){ + var self = this; + + if (self.slide.completed) { + return; + } + + if (event.data !== YT.PlayerState.ENDED) { + if (!event.target.getCurrentTime) { + return; + } + + if (self.tid) { + clearInterval(self.tid); + } + + self.currentVideoTime = event.target.getCurrentTime(); + self.totalVideoTime = event.target.getDuration(); + self.tid = setInterval(function (){ + self.currentVideoTime += 1; + if (self.totalVideoTime && self.currentVideoTime > self.totalVideoTime - 30){ + clearInterval(self.tid); + if (!self.slide.hasQuestion && !self.slide.completed){ + self.trigger_up('slide_to_complete', self.slide); + } + } + }, 1000); + } else { + if (self.tid) { + clearInterval(self.tid); + } + this.player = undefined; + if (this.slide.hasNext) { + this.trigger_up('slide_go_next'); + } + } + }, + }); + + + /** + * This widget is responsible of navigation for one slide to another: + * - by clicking on any slide list entry + * - by mouse click (next / prev) + * - by recieving the order to go to prev/next slide (`goPrevious` and `goNext` public methods) + * + * The widget will trigger an event `change_slide` with + * the `slideId` and `isMiniQuiz` as data. + */ + var Sidebar = publicWidget.Widget.extend({ + events: { + "click .o_wslides_fs_sidebar_list_item": '_onClickTab', + }, + init: function (parent, slideList, defaultSlide) { + var result = this._super.apply(this, arguments); + this.slideEntries = slideList; + this.set('slideEntry', defaultSlide); + return result; + }, + start: function (){ + var self = this; + this.on('change:slideEntry', this, this._onChangeCurrentSlide); + return this._super.apply(this, arguments).then(function (){ + $(document).keydown(self._onKeyDown.bind(self)); + }); + }, + destroy: function () { + $(document).unbind('keydown', this._onKeyDown.bind(this)); + return this._super.apply(this, arguments); + }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + /** + * Change the current slide with the next one (if there is one). + * + * @public + */ + goNext: function () { + var currentIndex = this._getCurrentIndex(); + if (currentIndex < this.slideEntries.length-1) { + this.set('slideEntry', this.slideEntries[currentIndex+1]); + } + }, + /** + * Change the current slide with the previous one (if there is one). + * + * @public + */ + goPrevious: function () { + var currentIndex = this._getCurrentIndex(); + if (currentIndex >= 1) { + this.set('slideEntry', this.slideEntries[currentIndex-1]); + } + }, + /** + * Greens up the bullet when the slide is completed + * + * @public + * @param {Integer} slideId + */ + setSlideCompleted: function (slideId) { + var $elem = this.$('.fa-circle-thin[data-slide-id="'+slideId+'"]'); + $elem.removeClass('fa-circle-thin').addClass('fa-check text-success o_wslides_slide_completed'); + }, + /** + * Updates the progressbar whenever a lesson is completed + * + * @public + * @param {*} channelCompletion + */ + updateProgressbar: function (channelCompletion) { + var completion = Math.min(100, channelCompletion); + this.$('.progress-bar').css('width', completion + "%" ); + this.$('.o_wslides_progress_percentage').text(completion); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Get the index of the current slide entry (slide and/or quiz) + */ + _getCurrentIndex: function () { + var slide = this.get('slideEntry'); + var currentIndex = _.findIndex(this.slideEntries, function (entry) { + return entry.id === slide.id && entry.isQuiz === slide.isQuiz; + }); + return currentIndex; + }, + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + /** + * Handler called whenever the user clicks on a sub-quiz which is linked to a slide. + * This does NOT handle the case of a slide of type "quiz". + * By going through this handler, the widget will be able to determine that it has to render + * the associated quiz and not the main content. + * + * @private + * @param {*} ev + */ + _onClickMiniQuiz: function (ev){ + var slideID = parseInt($(ev.currentTarget).data().slide_id); + this.set('slideEntry',{ + slideID: slideID, + isMiniQuiz: true + }); + this.trigger_up('change_slide', this.get('slideEntry')); + }, + /** + * Handler called when the user clicks on a normal slide tab + * + * @private + * @param {*} ev + */ + _onClickTab: function (ev) { + ev.stopPropagation(); + var $elem = $(ev.currentTarget); + if ($elem.data('canAccess') === 'True') { + var isQuiz = $elem.data('isQuiz'); + var slideID = parseInt($elem.data('id')); + var slide = findSlide(this.slideEntries, {id: slideID, isQuiz: isQuiz}); + this.set('slideEntry', slide); + } + }, + /** + * Actively changes the active tab in the sidebar so that it corresponds + * the slide currently displayed + * + * @private + */ + _onChangeCurrentSlide: function () { + var slide = this.get('slideEntry'); + this.$('.o_wslides_fs_sidebar_list_item.active').removeClass('active'); + var selector = '.o_wslides_fs_sidebar_list_item[data-id='+slide.id+'][data-is-quiz!="1"]'; + + this.$(selector).addClass('active'); + this.trigger_up('change_slide', this.get('slideEntry')); + }, + + /** + * Binds left and right arrow to allow the user to navigate between slides + * + * @param {*} ev + * @private + */ + _onKeyDown: function (ev){ + switch (ev.key){ + case "ArrowLeft": + this.goPrevious(); + break; + case "ArrowRight": + this.goNext(); + break; + } + }, + }); + + var ShareDialog = Dialog.extend({ + template: 'website.slide.share.modal', + events: { + 'click .o_wslides_js_share_email button': '_onShareByEmailClick', + 'click a.o_wslides_js_social_share': '_onSlidesSocialShare', + 'click .o_clipboard_button': '_onShareLinkCopy', + }, + + init: function (parent, options, slide) { + options = _.defaults(options || {}, { + title: "Share", + buttons: [{text: "Cancel", close: true}], + size: 'medium', + }); + this._super(parent, options); + this.slide = slide; + this.session = session; + }, + + _onShareByEmailClick: function() { + var form = this.$('.o_wslides_js_share_email'); + var input = form.find('input'); + var slideID = form.find('button').data('slide-id'); + if (input.val() && input[0].checkValidity()) { + form.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + this._rpc({ + route: '/slides/slide/send_share_email', + params: { + slide_id: slideID, + email: input.val(), + fullscreen: true + }, + }).then(function () { + form.html('<div class="alert alert-info" role="alert">' + _t('<strong>Thank you!</strong> Mail has been sent.') + '</div>'); + }); + } else { + form.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + input.focus(); + } + }, + + _onSlidesSocialShare: function (ev) { + ev.preventDefault(); + var popUpURL = $(ev.currentTarget).attr('href'); + window.open(popUpURL, 'Share Dialog', 'width=626,height=436'); + }, + + _onShareLinkCopy: function (ev) { + ev.preventDefault(); + var $clipboardBtn = this.$('.o_clipboard_button'); + $clipboardBtn.tooltip({title: "Copied !", trigger: "manual", placement: "bottom"}); + var self = this; + var clipboard = new ClipboardJS('.o_clipboard_button', { + target: function () { + return self.$('.o_wslides_js_share_link')[0]; + }, + container: this.el + }); + clipboard.on('success', function () { + clipboard.destroy(); + $clipboardBtn.tooltip('show'); + _.delay(function () { + $clipboardBtn.tooltip("hide"); + }, 800); + }); + clipboard.on('error', function (e) { + clipboard.destroy(); + }) + }, + + }); + + var ShareButton = publicWidget.Widget.extend({ + events: { + "click .o_wslides_fs_share": '_onClickShareSlide' + }, + + init: function (el, slide) { + var result = this._super.apply(this, arguments); + this.slide = slide; + return result; + }, + + _openDialog: function() { + return new ShareDialog(this, {}, this.slide).open(); + }, + + _onClickShareSlide: function (ev) { + ev.preventDefault(); + this._openDialog(); + }, + + _onChangeSlide: function (currentSlide) { + this.slide = currentSlide; + } + + }); + + /** + * This widget's purpose is to show content of a course, naviguating through contents + * and correclty display it. It also handle slide completion, course progress, ... + * + * This widget is rendered sever side, and attached to the existing DOM. + */ + var Fullscreen = publicWidget.Widget.extend({ + events: { + "click .o_wslides_fs_toggle_sidebar": '_onClickToggleSidebar', + }, + custom_events: { + 'change_slide': '_onChangeSlideRequest', + 'slide_to_complete': '_onSlideToComplete', + 'slide_completed': '_onSlideCompleted', + 'slide_go_next': '_onSlideGoToNext', + }, + /** + * @override + * @param {Object} el + * @param {Object} slides Contains the list of all slides of the course + * @param {integer} defaultSlideId Contains the ID of the slide requested by the user + */ + init: function (parent, slides, defaultSlideId, channelData){ + var result = this._super.apply(this,arguments); + this.initialSlideID = defaultSlideId; + this.slides = this._preprocessSlideData(slides); + this.channel = channelData; + var slide; + var urlParams = $.deparam.querystring(); + if (defaultSlideId) { + slide = findSlide(this.slides, {id: defaultSlideId, isQuiz: urlParams.quiz === "1" }); + } else { + slide = this.slides[0]; + } + + this.set('slide', slide); + + this.sidebar = new Sidebar(this, this.slides, slide); + this.shareButton = new ShareButton(this, slide); + return result; + }, + /** + * @override + */ + start: function (){ + var self = this; + this.on('change:slide', this, this._onChangeSlide); + this._toggleSidebar(); + return this._super.apply(this, arguments).then(function () { + return self._onChangeSlide(); // trigger manually once DOM ready, since slide content is not rendered server side + }); + }, + /** + * Extended to attach sub widget to sub DOM. This might be experimental but + * seems working fine. + * + * @override + */ + attachTo: function (){ + var defs = [this._super.apply(this, arguments)]; + defs.push(this.sidebar.attachTo(this.$('.o_wslides_fs_sidebar'))); + defs.push(this.shareButton.attachTo(this.$('.o_wslides_slide_fs_header'))); + return $.when.apply($, defs); + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Fetches content with an rpc call for slides of type "webpage" + * + * @private + */ + _fetchHtmlContent: function (){ + var self = this; + var currentSlide = this.get('slide'); + return self._rpc({ + route:"/slides/slide/get_html_content", + params: { + 'slide_id': currentSlide.id + } + }).then(function (data){ + if (data.html_content) { + currentSlide.htmlContent = data.html_content; + } + }); + }, + /** + * Fetches slide content depending on its type. + * If the slide doesn't need to fetch any content, return a resolved deferred + * + * @private + */ + _fetchSlideContent: function (){ + var slide = this.get('slide'); + if (slide.type === 'webpage' && !slide.isQuiz) { + return this._fetchHtmlContent(); + } + return Promise.resolve(); + }, + _markAsCompleted: function (slideId, completion) { + var slide = findSlide(this.slides, {id: slideId}); + slide.completed = true; + this.sidebar.setSlideCompleted(slide.id); + this.sidebar.updateProgressbar(completion); + }, + /** + * Extend the slide data list to add informations about rendering method, and other + * specific values according to their slide_type. + */ + _preprocessSlideData: function (slidesDataList) { + slidesDataList.forEach(function (slideData, index) { + // compute hasNext slide + slideData.hasNext = index < slidesDataList.length-1; + // compute embed url + if (slideData.type === 'video') { + slideData.embedCode = $(slideData.embedCode).attr('src') || ""; // embedCode contains an iframe tag, where src attribute is the url (youtube or embed document from odoo) + var separator = slideData.embedCode.indexOf("?") !== -1 ? "&" : "?"; + var scheme = slideData.embedCode.indexOf('//') === 0 ? 'https:' : ''; + var params = { rel: 0, enablejsapi: 1, origin: window.location.origin }; + if (slideData.embedCode.indexOf("//drive.google.com") === -1) { + params.autoplay = 1; + } + slideData.embedUrl = slideData.embedCode ? scheme + slideData.embedCode + separator + $.param(params) : ""; + } else if (slideData.type === 'infographic') { + slideData.embedUrl = _.str.sprintf('/web/image/slide.slide/%s/image_1024', slideData.id); + } else if (_.contains(['document', 'presentation'], slideData.type)) { + slideData.embedUrl = $(slideData.embedCode).attr('src'); + } + // fill empty property to allow searching on it with _.filter(list, matcher) + slideData.isQuiz = !!slideData.isQuiz; + slideData.hasQuestion = !!slideData.hasQuestion; + // technical settings for the Fullscreen to work + slideData._autoSetDone = _.contains(['infographic', 'presentation', 'document', 'webpage'], slideData.type) && !slideData.hasQuestion; + }); + return slidesDataList; + }, + /** + * Changes the url whenever the user changes slides. + * This allows the user to refresh the page and stay on the right slide + * + * @private + */ + _pushUrlState: function (){ + var urlParts = window.location.pathname.split('/'); + urlParts[urlParts.length-1] = this.get('slide').slug; + var url = urlParts.join('/'); + this.$('.o_wslides_fs_exit_fullscreen').attr('href', url); + var params = {'fullscreen': 1 }; + if (this.get('slide').isQuiz){ + params.quiz = 1; + } + var fullscreenUrl = _.str.sprintf('%s?%s', url, $.param(params)); + history.pushState(null, '', fullscreenUrl); + }, + /** + * Render the current slide content using specific mecanism according to slide type: + * - simply append content (for webpage) + * - template rendering (for image, document, ....) + * - using a sub widget (quiz and video) + * + * @private + * @returns Deferred + */ + _renderSlide: function () { + var slide = this.get('slide'); + var $content = this.$('.o_wslides_fs_content'); + $content.empty(); + + // display quiz slide, or quiz attached to a slide + if (slide.type === 'quiz' || slide.isQuiz) { + $content.addClass('bg-white'); + var QuizWidget = new Quiz(this, slide, this.channel); + return QuizWidget.appendTo($content); + } + + // render slide content + if (_.contains(['document', 'presentation', 'infographic'], slide.type)) { + $content.html(QWeb.render('website.slides.fullscreen.content', {widget: this})); + } else if (slide.type === 'video') { + this.videoPlayer = new VideoPlayer(this, slide); + return this.videoPlayer.appendTo($content); + } else if (slide.type === 'webpage'){ + var $wpContainer = $('<div>').addClass('o_wslide_fs_webpage_content bg-white block w-100 overflow-auto'); + $(slide.htmlContent).appendTo($wpContainer); + $content.append($wpContainer); + this.trigger_up('widgets_start_request', { + $target: $content, + }); + } + return Promise.resolve(); + }, + /** + * Once the completion conditions are filled, + * rpc call to set the the relation between the slide and the user as "completed" + * + * @private + * @param {Integer} slideId: the id of slide to set as completed + */ + _setCompleted: function (slideId){ + var self = this; + var slide = findSlide(this.slides, {id: slideId}); + if (!slide.completed) { // no useless RPC call + return this._rpc({ + route: '/slides/slide/set_completed', + params: { + slide_id: slide.id, + } + }).then(function (data){ + self._markAsCompleted(slideId, data.channel_completion); + return Promise.resolve(); + }); + } + return Promise.resolve(); + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * Triggered whenever the user changes slides. + * When the current slide is changed, widget will be automatically updated + * and allowed to: fetch the content if needed, render it, update the url, + * and set slide as "completed" according to its type requirements. In + * mobile case (i.e. limited screensize), sidebar will be toggled since + * sidebar will block most or all of new slide visibility. + * + * @private + */ + _onChangeSlide: function () { + var self = this; + var slide = this.get('slide'); + self._pushUrlState(); + return this._fetchSlideContent().then(function() { // render content + var websiteName = document.title.split(" | ")[1]; // get the website name from title + document.title = (websiteName) ? slide.name + ' | ' + websiteName : slide.name; + if (config.device.size_class < config.device.SIZES.MD) { + self._toggleSidebar(); // hide sidebar when small device screen + } + return self._renderSlide(); + }).then(function() { + if (slide._autoSetDone && !session.is_website_user) { // no useless RPC call + if (['document', 'presentation'].includes(slide.type)) { + // only set the slide as completed after iFrame is loaded to avoid concurrent execution with 'embedUrl' controller + self.el.querySelector('iframe.o_wslides_iframe_viewer').addEventListener('load', () => self._setCompleted(slide.id)); + } else { + return self._setCompleted(slide.id); + } + } + }); + }, + /** + * Changes current slide when receiving custom event `change_slide` with + * its id and if it's its quizz or not we need to display. + * + * @private + */ + _onChangeSlideRequest: function (ev){ + var slideData = ev.data; + var newSlide = findSlide(this.slides, { + id: slideData.id, + isQuiz: slideData.isQuiz || false, + }); + this.set('slide', newSlide); + this.shareButton._onChangeSlide(newSlide); + }, + /** + * Triggered when subwidget has mark the slide as done, and the UI need to be adapted. + * + * @private + */ + _onSlideCompleted: function (ev) { + var slide = ev.data.slide; + var completion = ev.data.completion; + this._markAsCompleted(slide.id, completion); + }, + /** + * Triggered when sub widget business is done and that slide + * can now be marked as done. + * + * @private + */ + _onSlideToComplete: function (ev) { + if (!session.is_website_user) { // no useless RPC call + var slideId = ev.data.id; + this._setCompleted(slideId); + } + }, + /** + * Go the next slide + * + * @private + */ + _onSlideGoToNext: function (ev) { + this.sidebar.goNext(); + }, + /** + * Called when the sidebar toggle is clicked -> toggles the sidebar visibility. + * + * @private + */ + _onClickToggleSidebar: function (ev){ + ev.preventDefault(); + this._toggleSidebar(); + }, + /** + * Toggles sidebar visibility. + * + * @private + */ + _toggleSidebar: function () { + this.$('.o_wslides_fs_sidebar').toggleClass('o_wslides_fs_sidebar_hidden'); + this.$('.o_wslides_fs_toggle_sidebar').toggleClass('active'); + }, + }); + + publicWidget.registry.websiteSlidesFullscreenPlayer = publicWidget.Widget.extend({ + selector: '.o_wslides_fs_main', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml', '/website_slides/static/src/xml/website_slides_share.xml'], + start: function (){ + var self = this; + var proms = [this._super.apply(this, arguments)]; + var fullscreen = new Fullscreen(this, this._getSlides(), this._getCurrentSlideID(), this._extractChannelData()); + proms.push(fullscreen.attachTo(".o_wslides_fs_main")); + return Promise.all(proms).then(function () { + $('#edit-page-menu a[data-action="edit"]').on('click', self._onWebEditorClick.bind(self)); + }); + }, + + /** + * The web editor does not work well with the e-learning fullscreen view. + * It actually completely closes the fullscreen view and opens the edition on a blank page. + * + * To avoid this, we intercept the click on the 'edit' button and redirect to the + * non-fullscreen view of this slide with the editor enabled, which is more suited to edit + * in-place anyway. + * + * @param {MouseEvent} e + */ + _onWebEditorClick: function (e) { + e.preventDefault(); + e.stopPropagation(); + + window.location = `${window.location.pathname}?fullscreen=0&enable_editor=1`; + }, + + _extractChannelData: function (){ + return this.$el.data(); + }, + _getCurrentSlideID: function (){ + return parseInt(this.$('.o_wslides_fs_sidebar_list_item.active').data('id')); + }, + /** + * @private + * Creates slides objects from every slide-list-cells attributes + */ + _getSlides: function (){ + var $slides = this.$('.o_wslides_fs_sidebar_list_item[data-can-access="True"]'); + var slideList = []; + $slides.each(function () { + var slideData = $(this).data(); + slideList.push(slideData); + }); + return slideList; + }, + }); + + return Fullscreen; +}); diff --git a/addons/website_slides/static/src/js/slides_course_join.js b/addons/website_slides/static/src/js/slides_course_join.js new file mode 100644 index 00000000..0817f372 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_join.js @@ -0,0 +1,161 @@ +odoo.define('website_slides.course.join.widget', function (require) { +'use strict'; + +var core = require('web.core'); +var publicWidget = require('web.public.widget'); + +var _t = core._t; + +var CourseJoinWidget = publicWidget.Widget.extend({ + template: 'slide.course.join', + xmlDependencies: ['/website_slides/static/src/xml/slide_course_join.xml'], + events: { + 'click .o_wslides_js_course_join_link': '_onClickJoin', + }, + + /** + * + * Overridden to add options parameters. + * + * @param {Object} parent + * @param {Object} options + * @param {Object} options.channel slide.channel information + * @param {boolean} options.isMember whether current user is member or not + * @param {boolean} options.publicUser whether current user is public or not + * @param {string} [options.joinMessage] the message to use for the simple join case + * when the course if free and the user is logged in, defaults to "Join Course". + * @param {Promise} [options.beforeJoin] a promise to execute before we redirect to + * another url within the join process (login / buy course / ...) + * @param {function} [options.afterJoin] a callback function called after the user has + * joined the course + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.channel = options.channel; + this.isMember = options.isMember; + this.publicUser = options.publicUser; + this.joinMessage = options.joinMessage || _t('Join Course'), + this.beforeJoin = options.beforeJoin || Promise.resolve(); + this.afterJoin = options.afterJoin || function () {document.location.reload();}; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickJoin: function (ev) { + ev.preventDefault(); + + if (this.channel.channelEnroll !== 'invite') { + if (this.publicUser) { + this.beforeJoin().then(this._redirectToLogin.bind(this)); + } else if (!this.isMember && this.channel.channelEnroll === 'public') { + this.joinChannel(this.channel.channelId); + } + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Builds a login page that then redirects to this slide page, or the channel if the course + * is not configured as public enroll type. + * + * @private + */ + _redirectToLogin: function () { + var url; + if (this.channel.channelEnroll === 'public') { + url = window.location.pathname; + if (document.location.href.indexOf("fullscreen") !== -1) { + url += '?fullscreen=1'; + } + } else { + url = `/slides/${this.channel.channelId}`; + } + document.location = _.str.sprintf('/web/login?redirect=%s', encodeURIComponent(url)); + }, + + /** + * @private + * @param {Object} $el + * @param {String} message + */ + _popoverAlert: function ($el, message) { + $el.popover({ + trigger: 'focus', + placement: 'bottom', + container: 'body', + html: true, + content: function () { + return message; + } + }).popover('show'); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + /** + * @public + * @param {integer} channelId + */ + joinChannel: function (channelId) { + var self = this; + this._rpc({ + route: '/slides/channel/join', + params: { + channel_id: channelId, + }, + }).then(function (data) { + if (!data.error) { + self.afterJoin(); + } else { + if (data.error === 'public_user') { + var message = _t('Please <a href="/web/login?redirect=%s">login</a> to join this course'); + var signupAllowed = data.error_signup_allowed || false; + if (signupAllowed) { + message = _t('Please <a href="/web/signup?redirect=%s">create an account</a> to join this course'); + } + self._popoverAlert(self.$el, _.str.sprintf(message, (document.URL))); + } else if (data.error === 'join_done') { + self._popoverAlert(self.$el, _t('You have already joined this channel')); + } else { + self._popoverAlert(self.$el, _t('Unknown error')); + } + } + }); + }, +}); + +publicWidget.registry.websiteSlidesCourseJoin = publicWidget.Widget.extend({ + selector: '.o_wslides_js_course_join_link', + + /** + * @override + * @param {Object} parent + */ + start: function () { + var self = this; + var proms = [this._super.apply(this, arguments)]; + var data = self.$el.data(); + var options = {channel: {channelEnroll: data.channelEnroll, channelId: data.channelId}}; + $('.o_wslides_js_course_join').each(function () { + proms.push(new CourseJoinWidget(self, options).attachTo($(this))); + }); + return Promise.all(proms); + }, +}); + +return { + courseJoinWidget: CourseJoinWidget, + websiteSlidesCourseJoin: publicWidget.registry.websiteSlidesCourseJoin +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js new file mode 100644 index 00000000..6d2fd355 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -0,0 +1,775 @@ +odoo.define('website_slides.quiz', function (require) { + 'use strict'; + + var publicWidget = require('web.public.widget'); + var Dialog = require('web.Dialog'); + var core = require('web.core'); + var session = require('web.session'); + + var CourseJoinWidget = require('website_slides.course.join.widget').courseJoinWidget; + var QuestionFormWidget = require('website_slides.quiz.question.form'); + var SlideQuizFinishModal = require('website_slides.quiz.finish'); + + var SlideEnrollDialog = require('website_slides.course.enroll').slideEnrollDialog; + + var QWeb = core.qweb; + var _t = core._t; + + /** + * This widget is responsible of displaying quiz questions and propositions. Submitting the quiz will fetch the + * correction and decorate the answers according to the result. Error message or modal can be displayed. + * + * This widget can be attached to DOM rendered server-side by `website_slides.slide_type_quiz` or + * used client side (Fullscreen). + * + * Triggered events are : + * - slide_go_next: need to go to the next slide, when quiz is done. Event data contains the current slide id. + * - quiz_completed: when the quiz is passed and completed by the user. Event data contains current slide data. + */ + var Quiz = publicWidget.Widget.extend({ + template: 'slide.slide.quiz', + xmlDependencies: [ + '/website_slides/static/src/xml/slide_quiz.xml', + '/website_slides/static/src/xml/slide_course_join.xml' + ], + events: { + "click .o_wslides_quiz_answer": '_onAnswerClick', + "click .o_wslides_js_lesson_quiz_submit": '_submitQuiz', + "click .o_wslides_quiz_modal_btn": '_onClickNext', + "click .o_wslides_quiz_continue": '_onClickNext', + "click .o_wslides_js_lesson_quiz_reset": '_onClickReset', + 'click .o_wslides_js_quiz_add': '_onCreateQuizClick', + 'click .o_wslides_js_quiz_edit_question': '_onEditQuestionClick', + 'click .o_wslides_js_quiz_delete_question': '_onDeleteQuestionClick', + 'click .o_wslides_js_channel_enroll': '_onSendRequestToResponsibleClick', + }, + + custom_events: { + display_created_question: '_displayCreatedQuestion', + display_updated_question: '_displayUpdatedQuestion', + reset_display: '_resetDisplay', + delete_question: '_deleteQuestion', + }, + + /** + * @override + * @param {Object} parent + * @param {Object} slide_data holding all the classic slide information + * @param {Object} quiz_data : optional quiz data to display. If not given, will be fetched. (questions and answers). + */ + init: function (parent, slide_data, channel_data, quiz_data) { + this._super.apply(this, arguments); + this.slide = _.defaults(slide_data, { + id: 0, + name: '', + hasNext: false, + completed: false, + isMember: false, + }); + this.quiz = quiz_data || false; + if (this.quiz) { + this.quiz.questionsCount = quiz_data.questions.length; + } + this.isMember = slide_data.isMember || false; + this.publicUser = session.is_website_user; + this.userId = session.user_id; + this.redirectURL = encodeURIComponent(document.URL); + this.channel = channel_data; + }, + + /** + * @override + */ + willStart: function () { + var defs = [this._super.apply(this, arguments)]; + if (!this.quiz) { + defs.push(this._fetchQuiz()); + } + return Promise.all(defs); + }, + + /** + * Overridden to add custom rendering behavior upon start of the widget. + * + * If the user has answered the quiz before having joined the course, we check + * his answers (saved into his session) here as well. + * + * @override + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._renderValidationInfo(); + self._bindSortable(); + self._checkLocationHref(); + if (!self.isMember) { + self._renderJoinWidget(); + } else if (self.slide.sessionAnswers) { + self._applySessionAnswers(); + self._submitQuiz(); + } + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _alertShow: function (alertCode) { + var message = _t('There was an error validating this quiz.'); + if (alertCode === 'slide_quiz_incomplete') { + message = _t('All questions must be answered !'); + } else if (alertCode === 'slide_quiz_done') { + message = _t('This quiz is already done. Retaking it is not possible.'); + } else if (alertCode === 'public_user') { + message = _t('You must be logged to submit the quiz.'); + } + + this.displayNotification({ + type: 'warning', + message: message, + sticky: true + }); + }, + + /** + * Allows to reorder the questions + * @private + */ + _bindSortable: function () { + this.$el.sortable({ + handle: '.o_wslides_js_quiz_sequence_handler', + items: '.o_wslides_js_lesson_quiz_question', + stop: this._reorderQuestions.bind(this), + placeholder: 'o_wslides_js_quiz_sequence_highlight position-relative my-3' + }); + }, + + /** + * Get all the questions ID from the displayed Quiz + * @returns {Array} + * @private + */ + _getQuestionsIds: function () { + return this.$('.o_wslides_js_lesson_quiz_question').map(function () { + return $(this).data('question-id'); + }).get(); + }, + + /** + * Modify visually the sequence of all the questions after + * calling the _reorderQuestions RPC call. + * @private + */ + _modifyQuestionsSequence: function () { + this.$('.o_wslides_js_lesson_quiz_question').each(function (index, question) { + $(question).find('span.o_wslides_quiz_question_sequence').text(index + 1); + }); + }, + + /** + * RPC call to resequence all the questions. It is called + * after modifying the sequence of a question and also after + * deleting a question. + * @private + */ + _reorderQuestions: function () { + this._rpc({ + route: '/web/dataset/resequence', + params: { + model: "slide.question", + ids: this._getQuestionsIds() + } + }).then(this._modifyQuestionsSequence.bind(this)) + }, + /* + * @private + * Fetch the quiz for a particular slide + */ + _fetchQuiz: function () { + var self = this; + return self._rpc({ + route:'/slides/slide/quiz/get', + params: { + 'slide_id': self.slide.id, + } + }).then(function (quiz_data) { + self.quiz = { + questions: quiz_data.slide_questions || [], + questionsCount: quiz_data.slide_questions.length, + quizAttemptsCount: quiz_data.quiz_attempts_count || 0, + quizKarmaGain: quiz_data.quiz_karma_gain || 0, + quizKarmaWon: quiz_data.quiz_karma_won || 0, + }; + }); + }, + + /** + * Hide the edit and delete button and also the handler + * to resequence the question + * @private + */ + _hideEditOptions: function () { + this.$('.o_wslides_js_lesson_quiz_question .o_wslides_js_quiz_edit_del,' + + ' .o_wslides_js_lesson_quiz_question .o_wslides_js_quiz_sequence_handler').addClass('d-none'); + }, + + /** + * @private + * Decorate the answers according to state + */ + _disableAnswers: function () { + var self = this; + this.$('.o_wslides_js_lesson_quiz_question').addClass('completed-disabled'); + this.$('input[type=radio]').each(function () { + $(this).prop('disabled', self.slide.completed); + }); + }, + + /** + * Decorate the answer inputs according to the correction and adds the answer comment if + * any. + * + * @private + */ + _renderAnswersHighlightingAndComments: function () { + var self = this; + this.$('.o_wslides_js_lesson_quiz_question').each(function () { + var $question = $(this); + var questionId = $question.data('questionId'); + var isCorrect = self.quiz.answers[questionId].is_correct; + $question.find('a.o_wslides_quiz_answer').each(function () { + var $answer = $(this); + $answer.find('i.fa').addClass('d-none'); + if ($answer.find('input[type=radio]')[0].checked) { + if (isCorrect) { + $answer.removeClass('list-group-item-danger').addClass('list-group-item-success'); + $answer.find('i.fa-check-circle').removeClass('d-none'); + } else { + $answer.removeClass('list-group-item-success').addClass('list-group-item-danger'); + $answer.find('i.fa-times-circle').removeClass('d-none'); + $answer.find('label input').prop('checked', false); + } + } else { + $answer.removeClass('list-group-item-danger list-group-item-success'); + $answer.find('i.fa-circle').removeClass('d-none'); + } + }); + var comment = self.quiz.answers[questionId].comment; + if (comment) { + $question.find('.o_wslides_quiz_answer_info').removeClass('d-none'); + $question.find('.o_wslides_quiz_answer_comment').text(comment); + } + }); + }, + + /** + * Will check if we have answers coming from the session and re-apply them. + */ + _applySessionAnswers: function () { + if (!this.slide.sessionAnswers || this.slide.sessionAnswers.length === 0) { + return; + } + + var self = this; + this.$('.o_wslides_js_lesson_quiz_question').each(function () { + var $question = $(this); + $question.find('a.o_wslides_quiz_answer').each(function () { + var $answer = $(this); + if (!$answer.find('input[type=radio]')[0].checked && + _.contains(self.slide.sessionAnswers, $answer.data('answerId'))) { + $answer.find('input[type=radio]').prop('checked', true); + } + }); + }); + + // reset answers coming from the session + this.slide.sessionAnswers = false; + }, + + /* + * @private + * Update validation box (karma, buttons) according to widget state + */ + _renderValidationInfo: function () { + var $validationElem = this.$('.o_wslides_js_lesson_quiz_validation'); + $validationElem.html( + QWeb.render('slide.slide.quiz.validation', {'widget': this}) + ); + }, + + /** + * Renders the button to join a course. + * If the user is logged in, the course is public, and the user has previously tried to + * submit answers, we automatically attempt to join the course. + * + * @private + */ + _renderJoinWidget: function () { + var $widgetLocation = this.$(".o_wslides_join_course_widget"); + if ($widgetLocation.length !== 0) { + var courseJoinWidget = new CourseJoinWidget(this, { + isQuiz: true, + channel: this.channel, + isMember: this.isMember, + publicUser: this.publicUser, + beforeJoin: this._saveQuizAnswersToSession.bind(this), + afterJoin: this._afterJoin.bind(this), + joinMessage: _t('Join & Submit'), + }); + + courseJoinWidget.appendTo($widgetLocation); + if (!this.publicUser && courseJoinWidget.channel.channelEnroll === 'public' && this.slide.sessionAnswers) { + courseJoinWidget.joinChannel(this.channel.channelId); + } + } + }, + + /** + * Get the quiz answers filled in by the User + * + * @private + */ + _getQuizAnswers: function () { + return this.$('input[type=radio]:checked').map(function (index, element) { + return parseInt($(element).val()); + }).get(); + }, + + /** + * Submit a quiz and get the correction. It will display messages + * according to quiz result. + * + * @private + */ + _submitQuiz: function () { + var self = this; + + return this._rpc({ + route: '/slides/slide/quiz/submit', + params: { + slide_id: self.slide.id, + answer_ids: this._getQuizAnswers(), + } + }).then(function (data) { + if (data.error) { + self._alertShow(data.error); + } else { + self.quiz = _.extend(self.quiz, data); + if (data.completed) { + self._disableAnswers(); + new SlideQuizFinishModal(self, { + quiz: self.quiz, + hasNext: self.slide.hasNext, + userId: self.userId + }).open(); + self.slide.completed = true; + self.trigger_up('slide_completed', {slide: self.slide, completion: data.channel_completion}); + } + self._hideEditOptions(); + self._renderAnswersHighlightingAndComments(); + self._renderValidationInfo(); + } + }); + }, + + /** + * Get all the question information after clicking on + * the edit button + * @param $elem + * @returns {{id: *, sequence: number, text: *, answers: Array}} + * @private + */ + _getQuestionDetails: function ($elem) { + var answers = []; + $elem.find('.o_wslides_quiz_answer').each(function () { + answers.push({ + 'id': $(this).data('answerId'), + 'text_value': $(this).data('text'), + 'is_correct': $(this).data('isCorrect'), + 'comment': $(this).data('comment') + }); + }); + return { + 'id': $elem.data('questionId'), + 'sequence': parseInt($elem.find('.o_wslides_quiz_question_sequence').text()), + 'text': $elem.data('title'), + 'answers': answers, + }; + }, + + /** + * If the slides has been called with the Add Quiz button on the slide list + * it goes straight to the 'Add Quiz' button and clicks on it. + * @private + */ + _checkLocationHref: function () { + if (window.location.href.includes('quiz_quick_create')) { + this._onCreateQuizClick(); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * When clicking on an answer, this one should be marked as "checked". + * + * @private + * @param OdooEvent ev + */ + _onAnswerClick: function (ev) { + ev.preventDefault(); + if (!this.slide.completed) { + $(ev.currentTarget).find('input[type=radio]').prop('checked', true); + } + }, + + /** + * Triggering a event to switch to next slide + * + * @private + * @param OdooEvent ev + */ + _onClickNext: function (ev) { + if (this.slide.hasNext) { + this.trigger_up('slide_go_next'); + } + }, + + /** + * Resets the completion of the slide so the user can take + * the quiz again + * + * @private + */ + _onClickReset: function () { + this._rpc({ + route: '/slides/slide/quiz/reset', + params: { + slide_id: this.slide.id + } + }).then(function () { + window.location.reload(); + }); + }, + /** + * Saves the answers from the user and redirect the user to the + * specified url + * + * @private + */ + _saveQuizAnswersToSession: function () { + var quizAnswers = this._getQuizAnswers(); + if (quizAnswers.length === this.quiz.questions.length) { + return this._rpc({ + route: '/slides/slide/quiz/save_to_session', + params: { + 'quiz_answers': {'slide_id': this.slide.id, 'slide_answers': quizAnswers}, + } + }); + } else { + this._alertShow('slide_quiz_incomplete'); + return Promise.reject('The quiz is incomplete'); + } + }, + /** + * After joining the course, we immediately submit the quiz and get the correction. + * This allows a smooth onboarding when the user is logged in and the course is public. + * + * @private + */ + _afterJoin: function () { + this.isMember = true; + this._renderValidationInfo(); + this._applySessionAnswers(); + this._submitQuiz(); + }, + + /** + * When clicking on 'Add a Question' or 'Add Quiz' it + * initialize a new QuestionFormWidget to input the new + * question. + * @private + */ + _onCreateQuizClick: function () { + var $elem = this.$('.o_wslides_js_lesson_quiz_new_question'); + this.$('.o_wslides_js_quiz_add').addClass('d-none'); + new QuestionFormWidget(this, { + slideId: this.slide.id, + sequence: this.quiz.questionsCount + 1 + }).appendTo($elem); + }, + + /** + * When clicking on the edit button of a question it + * initialize a new QuestionFormWidget with the existing + * question as inputs. + * @param ev + * @private + */ + _onEditQuestionClick: function (ev) { + var $editedQuestion = $(ev.currentTarget).closest('.o_wslides_js_lesson_quiz_question'); + var question = this._getQuestionDetails($editedQuestion); + new QuestionFormWidget(this, { + editedQuestion: $editedQuestion, + question: question, + slideId: this.slide.id, + sequence: question.sequence, + update: true + }).insertAfter($editedQuestion); + $editedQuestion.hide(); + }, + + /** + * When clicking on the delete button of a question it + * toggles a modal to confirm the deletion + * @param ev + * @private + */ + _onDeleteQuestionClick: function (ev) { + var question = $(ev.currentTarget).closest('.o_wslides_js_lesson_quiz_question'); + new ConfirmationDialog(this, { + questionId: question.data('questionId'), + questionTitle: question.data('title') + }).open(); + }, + + /** + * Handler for the contact responsible link below a Quiz + * @param ev + * @private + */ + _onSendRequestToResponsibleClick: function(ev) { + ev.preventDefault(); + var channelId = $(ev.currentTarget).data('channelId'); + new SlideEnrollDialog(this, { + channelId: channelId, + $element: $(ev.currentTarget).closest('.alert.alert-info') + }).open(); + }, + + /** + * Displays the created Question at the correct place (after the last question or + * at the first place if there is no questions yet) It also displays the 'Add Question' + * button or open a new QuestionFormWidget if the user wants to immediately add another one. + * + * @param event + * @private + */ + _displayCreatedQuestion: function (event) { + var $lastQuestion = this.$('.o_wslides_js_lesson_quiz_question:last'); + if ($lastQuestion.length !== 0) { + $lastQuestion.after(event.data.newQuestionRenderedTemplate); + } else { + this.$el.prepend(event.data.newQuestionRenderedTemplate); + } + this.quiz.questionsCount++; + event.data.questionFormWidget.destroy(); + this.$('.o_wslides_js_quiz_add_question').removeClass('d-none'); + }, + + /** + * Replace the edited question by the new question and destroy + * the QuestionFormWidget. + * @param event + * @private + */ + _displayUpdatedQuestion: function (event) { + var questionFormWidget = event.data.questionFormWidget; + event.data.$editedQuestion.replaceWith(event.data.newQuestionRenderedTemplate); + questionFormWidget.destroy(); + }, + + /** + * If the user cancels the creation or update of a Question it resets the display + * of the updated Question or it displays back the buttons. + * + * @param event + * @private + */ + _resetDisplay: function (event) { + var questionFormWidget = event.data.questionFormWidget; + if (questionFormWidget.update) { + questionFormWidget.$editedQuestion.show(); + } else { + if (this.quiz.questionsCount > 0) { + this.$('.o_wslides_js_quiz_add_question').removeClass('d-none'); + } else { + this.$('.o_wslides_js_quiz_add_quiz').removeClass('d-none'); + } + } + questionFormWidget.destroy(); + }, + + /** + * After deletion of a Question the display is refreshed with the removal of the Question + * the reordering of all the remaining Questions and the change of the new Question sequence + * if the QuestionFormWidget is initialized. + * + * @param event + * @private + */ + _deleteQuestion: function (event) { + var questionId = event.data.questionId; + this.$('.o_wslides_js_lesson_quiz_question[data-question-id=' + questionId + ']').remove(); + this.quiz.questionsCount--; + this._reorderQuestions(); + var $newQuestionSequence = this.$('.o_wslides_js_lesson_quiz_new_question .o_wslides_quiz_question_sequence'); + $newQuestionSequence.text(parseInt($newQuestionSequence.text()) - 1); + if (this.quiz.questionsCount === 0 && !this.$('.o_wsildes_quiz_question_input').length) { + this.$('.o_wslides_js_quiz_add_quiz').removeClass('d-none'); + this.$('.o_wslides_js_quiz_add_question').addClass('d-none'); + this.$('.o_wslides_js_lesson_quiz_validation').addClass('d-none'); + } + }, + }); + + /** + * Dialog box shown when clicking the deletion button on a Question. + * When confirming it sends a RPC request to delete the Question. + */ + var ConfirmationDialog = Dialog.extend({ + template: 'slide.quiz.confirm.deletion', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website_slides/static/src/xml/slide_quiz_create.xml'] + ), + + /** + * @override + * @param parent + * @param options + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t('Delete Question'), + buttons: [ + { text: _t('Yes'), classes: 'btn-primary', click: this._onConfirmClick }, + { text: _t('No'), close: true} + ], + size: 'medium' + }); + this.questionId = options.questionId; + this.questionTitle = options.questionTitle; + this._super.apply(this, arguments); + }, + + /** + * Handler when the user confirm the deletion by clicking on 'Yes' + * it sends a RPC request to the server and triggers an event to + * visually delete the question. + * @private + */ + _onConfirmClick: function () { + var self = this; + this._rpc({ + model: 'slide.question', + method: 'unlink', + args: [this.questionId], + }).then(function () { + self.trigger_up('delete_question', { questionId: self.questionId }); + self.close(); + }); + } + }); + + publicWidget.registry.websiteSlidesQuizNoFullscreen = publicWidget.Widget.extend({ + selector: '.o_wslides_lesson_main', // selector of complete page, as we need slide content and aside content table + custom_events: { + slide_go_next: '_onQuizNextSlide', + slide_completed: '_onQuizCompleted', + }, + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + * @param {Object} parent + */ + start: function () { + var self = this; + this.quizWidgets = []; + var defs = [this._super.apply(this, arguments)]; + this.$('.o_wslides_js_lesson_quiz').each(function () { + var slideData = $(this).data(); + var channelData = self._extractChannelData(slideData); + slideData.quizData = { + questions: self._extractQuestionsAndAnswers(), + sessionAnswers: slideData.sessionAnswers || [], + quizKarmaMax: slideData.quizKarmaMax, + quizKarmaWon: slideData.quizKarmaWon || 0, + quizKarmaGain: slideData.quizKarmaGain, + quizAttemptsCount: slideData.quizAttemptsCount, + }; + defs.push(new Quiz(self, slideData, channelData, slideData.quizData).attachTo($(this))); + }); + return Promise.all(defs); + }, + + //---------------------------------------------------------------------- + // Handlers + //--------------------------------------------------------------------- + _onQuizCompleted: function (ev) { + var slide = ev.data.slide; + var completion = ev.data.completion; + this.$('#o_wslides_lesson_aside_slide_check_' + slide.id).addClass('text-success fa-check').removeClass('text-600 fa-circle-o'); + // need to use global selector as progress bar is outside this animation widget scope + $('.o_wslides_lesson_header .progress-bar').css('width', completion + "%"); + $('.o_wslides_lesson_header .progress span').text(_.str.sprintf("%s %%", completion)); + }, + _onQuizNextSlide: function () { + var url = this.$('.o_wslides_js_lesson_quiz').data('next-slide-url'); + window.location.replace(url); + }, + + //---------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------- + + _extractChannelData: function (slideData) { + return { + channelId: slideData.channelId, + channelEnroll: slideData.channelEnroll, + channelRequestedAccess: slideData.channelRequestedAccess || false, + signupAllowed: slideData.signupAllowed + }; + }, + + /** + * Extract data from exiting DOM rendered server-side, to have the list of questions with their + * relative answers. + * This method should return the same format as /slide/quiz/get controller. + * + * @return {Array<Object>} list of questions with answers + */ + _extractQuestionsAndAnswers: function () { + var questions = []; + this.$('.o_wslides_js_lesson_quiz_question').each(function () { + var $question = $(this); + var answers = []; + $question.find('.o_wslides_quiz_answer').each(function () { + var $answer = $(this); + answers.push({ + id: $answer.data('answerId'), + text: $answer.data('text'), + }); + }); + questions.push({ + id: $question.data('questionId'), + title: $question.data('title'), + answer_ids: answers, + }); + }); + return questions; + }, + }); + + return { + Quiz: Quiz, + ConfirmationDialog: ConfirmationDialog, + websiteSlidesQuizNoFullscreen: publicWidget.registry.websiteSlidesQuizNoFullscreen + }; +}); diff --git a/addons/website_slides/static/src/js/slides_course_quiz_finish.js b/addons/website_slides/static/src/js/slides_course_quiz_finish.js new file mode 100644 index 00000000..8d6d11e5 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_quiz_finish.js @@ -0,0 +1,157 @@ +odoo.define('website_slides.quiz.finish', function (require) { +'use strict'; + +var Dialog = require('web.Dialog'); +var core = require('web.core'); +var _t = core._t; + +/** + * This modal is used when the user finishes the quiz. + * It handles the animation of karma gain and leveling up by animating + * the progress bar and the text. + */ +var SlideQuizFinishModal = Dialog.extend({ + template: 'slide.slide.quiz.finish', + events: { + "click .o_wslides_quiz_modal_btn": '_onClickNext', + }, + + init: function(parent, options) { + var self = this; + this.quiz = options.quiz; + this.hasNext = options.hasNext; + this.userId = options.userId; + options = _.defaults(options || {}, { + size: 'medium', + dialogClass: 'd-flex p-0', + technical: false, + renderHeader: false, + renderFooter: false + }); + this._super.apply(this, arguments); + this.opened(function () { + self._animateProgressBar(); + self._animateText(); + }) + }, + + start: function() { + var self = this; + this._super.apply(this, arguments).then(function () { + self.$modal.addClass('o_wslides_quiz_modal pt-5'); + self.$modal.find('.modal-dialog').addClass('mt-5'); + self.$modal.find('.modal-content').addClass('shadow-lg'); + }); + }, + + //-------------------------------- + // Handlers + //-------------------------------- + + _onClickNext: function() { + this.trigger_up('slide_go_next'); + this.destroy(); + }, + + //-------------------------------- + // Private + //-------------------------------- + + /** + * Handles the animation of the karma gain in the following steps: + * 1. Initiate the tooltip which will display the actual Karma + * over the progress bar. + * 2. Animate the tooltip text to increment smoothly from the old + * karma value to the new karma value and updates it to make it + * move as the progress bar moves. + * 3a. The user doesn't level up + * I. When the user doesn't level up the progress bar simply goes + * from the old karma value to the new karma value. + * 3b. The user levels up + * I. The first step makes the progress bar go from the old karma + * value to 100%. + * II. The second step makes the progress bar go from 100% to 0%. + * III. The third and final step makes the progress bar go from 0% + * to the new karma value. It also changes the lower and upper + * bound to match the new rank. + * @param $modal + * @param rankProgress + * @private + */ + _animateProgressBar: function () { + var self = this; + this.$('[data-toggle="tooltip"]').tooltip({ + trigger: 'manual', + container: '.progress-bar-tooltip', + }).tooltip('show'); + + this.$('.tooltip-inner') + .prop('karma', this.quiz.rankProgress.previous_rank.karma) + .animate({ + karma: this.quiz.rankProgress.new_rank.karma + }, { + duration: this.quiz.rankProgress.level_up ? 1700 : 800, + step: function (newKarma) { + self.$('.tooltip-inner').text(Math.ceil(newKarma)); + self.$('[data-toggle="tooltip"]').tooltip('update'); + } + } + ); + + var $progressBar = this.$('.progress-bar'); + if (this.quiz.rankProgress.level_up) { + this.$('.o_wslides_quiz_modal_title').text(_t('Level up!')); + $progressBar.css('width', '100%'); + _.delay(function () { + self.$('.o_wslides_quiz_modal_rank_lower_bound') + .text(self.quiz.rankProgress.new_rank.lower_bound); + self.$('.o_wslides_quiz_modal_rank_upper_bound') + .text(self.quiz.rankProgress.new_rank.upper_bound || ""); + + // we need to use _.delay to force DOM re-rendering between 0 and new percentage + _.delay(function () { + $progressBar.addClass('no-transition').width('0%'); + }, 1); + _.delay(function () { + $progressBar + .removeClass('no-transition') + .width(self.quiz.rankProgress.new_rank.progress + '%'); + }, 100); + }, 800); + } else { + $progressBar.css('width', this.quiz.rankProgress.new_rank.progress + '%'); + } + }, + + /** + * Handles the animation of the different text such as the karma gain + * and the motivational message when the user levels up. + * @private + */ + _animateText: function () { + var self = this; + _.delay(function () { + self.$('h4.o_wslides_quiz_modal_xp_gained').addClass('show in'); + self.$('.o_wslides_quiz_modal_dismiss').removeClass('d-none'); + }, 800); + + if (this.quiz.rankProgress.level_up) { + _.delay(function () { + self.$('.o_wslides_quiz_modal_rank_motivational').addClass('fade'); + _.delay(function () { + self.$('.o_wslides_quiz_modal_rank_motivational').html( + self.quiz.rankProgress.last_rank ? + self.quiz.rankProgress.description : + self.quiz.rankProgress.new_rank.motivational + ); + self.$('.o_wslides_quiz_modal_rank_motivational').addClass('show in'); + }, 800); + }, 800); + } + }, + +}); + +return SlideQuizFinishModal; + +}); diff --git a/addons/website_slides/static/src/js/slides_course_quiz_question_form.js b/addons/website_slides/static/src/js/slides_course_quiz_question_form.js new file mode 100644 index 00000000..0fd43d43 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_quiz_question_form.js @@ -0,0 +1,228 @@ +odoo.define('website_slides.quiz.question.form', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var core = require('web.core'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * This Widget is responsible of displaying the question inputs when adding a new question or when updating an + * existing one. When validating the question it makes an RPC call to the server and trigger an event for + * displaying the question by the Quiz widget. + */ +var QuestionFormWidget = publicWidget.Widget.extend({ + template: 'slide.quiz.question.input', + xmlDependencies: ['/website_slides/static/src/xml/slide_quiz_create.xml'], + events: { + 'click .o_wslides_js_quiz_validate_question': '_validateQuestion', + 'click .o_wslides_js_quiz_cancel_question': '_cancelValidation', + 'click .o_wslides_js_quiz_comment_answer': '_toggleAnswerLineComment', + 'click .o_wslides_js_quiz_add_answer': '_addAnswerLine', + 'click .o_wslides_js_quiz_remove_answer': '_removeAnswerLine', + 'click .o_wslides_js_quiz_remove_answer_comment': '_removeAnswerLineComment', + 'change .o_wslides_js_quiz_answer_comment > input[type=text]': '_onCommentChanged' + }, + + /** + * @override + * @param parent + * @param options + */ + init: function (parent, options) { + this.$editedQuestion = options.editedQuestion; + this.question = options.question || {}; + this.update = options.update; + this.sequence = options.sequence; + this.slideId = options.slideId; + this._super.apply(this, arguments); + }, + + /** + * @override + * @returns {*} + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$('.o_wslides_quiz_question input').focus(); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * + * @param commentInput + * @private + */ + _onCommentChanged: function (event) { + var input = event.currentTarget; + var commentIcon = $(input).closest('.o_wslides_js_quiz_answer').find('.o_wslides_js_quiz_comment_answer'); + if (input.value.trim() !== '') { + commentIcon.addClass('text-primary'); + commentIcon.removeClass('text-muted'); + } else { + commentIcon.addClass('text-muted'); + commentIcon.removeClass('text-primary'); + } + }, + + /** + * Toggle the input for commenting the answer line which will be + * seen by the frontend user when submitting the quiz. + * @param ev + * @private + */ + _toggleAnswerLineComment: function (ev) { + var commentLine = $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').find('.o_wslides_js_quiz_answer_comment').toggleClass('d-none'); + commentLine.find('input[type=text]').focus(); + }, + + /** + * Adds a new answer line after the element the user clicked on + * e.g. If there is 3 answer lines and the user click on the add + * answer button on the second line, the new answer line will + * display between the second and the third line. + * @param ev + * @private + */ + _addAnswerLine: function (ev) { + $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').after(QWeb.render('slide.quiz.answer.line')); + }, + + /** + * Removes an answer line. Can't remove the last answer line. + * @param ev + * @private + */ + _removeAnswerLine: function (ev) { + if (this.$('.o_wslides_js_quiz_answer').length > 1) { + $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').remove(); + } + }, + + /** + * + * @param ev + * @private + */ + _removeAnswerLineComment: function (ev) { + var commentLine = $(ev.currentTarget).closest('.o_wslides_js_quiz_answer_comment').addClass('d-none'); + commentLine.find('input[type=text]').val('').change(); + }, + + /** + * Handler when user click on 'Save' or 'Update' buttons. + * @param ev + * @private + */ + _validateQuestion: function (ev) { + this._createOrUpdateQuestion({ + update: $(ev.currentTarget).hasClass('o_wslides_js_quiz_update'), + }); + }, + + /** + * Handler when user click on the 'Cancel' button. + * Calls a method from slides_course_quiz.js widget + * which will handle the reset of the question display. + * @private + */ + _cancelValidation: function () { + this.trigger_up('reset_display', { + questionFormWidget: this, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * RPC call to create or update a question. + * Triggers method from slides_course_quiz.js to + * correctly display the question. + * @param options + * @private + */ + _createOrUpdateQuestion: function (options) { + var self = this; + var $form = this.$('form'); + if (this._isValidForm($form)) { + var values = this._serializeForm($form); + this._rpc({ + route: '/slides/slide/quiz/question_add_or_update', + params: values + }).then(function (renderedQuestion) { + if (options.update) { + self.trigger_up('display_updated_question', { + newQuestionRenderedTemplate: renderedQuestion, + $editedQuestion: self.$editedQuestion, + questionFormWidget: self, + }); + } else { + self.trigger_up('display_created_question', { + newQuestionRenderedTemplate: renderedQuestion, + questionFormWidget: self + }); + } + }); + } else { + this.displayNotification({ + type: 'warning', + message: _t('Please fill in the question'), + sticky: true + }); + this.$('.o_wslides_quiz_question input').focus(); + } + }, + + /** + * Check if the Question has been filled up + * @param $form + * @returns {boolean} + * @private + */ + _isValidForm: function($form) { + return $form.find('.o_wslides_quiz_question input[type=text]').val().trim() !== ""; + }, + + /** + * Serialize the form into a JSON object to send it + * to the server through a RPC call. + * @param $form + * @returns {{id: *, sequence: *, question: *, slide_id: *, answer_ids: Array}} + * @private + */ + _serializeForm: function ($form) { + var answers = []; + var sequence = 1; + $form.find('.o_wslides_js_quiz_answer').each(function () { + var value = $(this).find('.o_wslides_js_quiz_answer_value').val(); + if (value.trim() !== "") { + var answer = { + 'sequence': sequence++, + 'text_value': value, + 'is_correct': $(this).find('input[type=radio]').prop('checked') === true, + 'comment': $(this).find('.o_wslides_js_quiz_answer_comment > input[type=text]').val().trim() + }; + answers.push(answer); + } + }); + return { + 'existing_question_id': this.$el.data('id'), + 'sequence': this.sequence, + 'question': $form.find('.o_wslides_quiz_question input[type=text]').val(), + 'slide_id': this.slideId, + 'answer_ids': answers + }; + }, + +}); + +return QuestionFormWidget; +}); diff --git a/addons/website_slides/static/src/js/slides_course_slides_list.js b/addons/website_slides/static/src/js/slides_course_slides_list.js new file mode 100644 index 00000000..fa45200a --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_slides_list.js @@ -0,0 +1,114 @@ +odoo.define('website_slides.course.slides.list', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var core = require('web.core'); +var _t = core._t; + +publicWidget.registry.websiteSlidesCourseSlidesList = publicWidget.Widget.extend({ + selector: '.o_wslides_slides_list', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'], + + start: function () { + this._super.apply(this,arguments); + + this.channelId = this.$el.data('channelId'); + + this._updateHref(); + this._bindSortable(); + }, + + //-------------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------------, + + /** + * Bind the sortable jQuery widget to both + * - course sections + * - course slides + * + * @private + */ + _bindSortable: function () { + this.$('ul.o_wslides_js_slides_list_container').sortable({ + handle: '.o_wslides_slides_list_drag', + stop: this._reorderSlides.bind(this), + items: '.o_wslides_slide_list_category', + placeholder: 'o_wslides_slides_list_slide_hilight position-relative mb-1' + }); + + this.$('.o_wslides_js_slides_list_container ul').sortable({ + handle: '.o_wslides_slides_list_drag', + connectWith: '.o_wslides_js_slides_list_container ul', + stop: this._reorderSlides.bind(this), + items: '.o_wslides_slides_list_slide:not(.o_wslides_js_slides_list_empty)', + placeholder: 'o_wslides_slides_list_slide_hilight position-relative mb-1' + }); + }, + + /** + * This method will check that a section is empty/not empty + * when the slides are reordered and show/hide the + * "Empty category" placeholder. + * + * @private + */ + _checkForEmptySections: function (){ + this.$('.o_wslides_slide_list_category').each(function (){ + var $categoryHeader = $(this).find('.o_wslides_slide_list_category_header'); + var categorySlideCount = $(this).find('.o_wslides_slides_list_slide:not(.o_not_editable)').length; + var $emptyFlagContainer = $categoryHeader.find('.o_wslides_slides_list_drag').first(); + var $emptyFlag = $emptyFlagContainer.find('small'); + if (categorySlideCount === 0 && $emptyFlag.length === 0){ + $emptyFlagContainer.append($('<small>', { + 'class': "ml-1 text-muted font-weight-bold", + text: _t("(empty)") + })); + } else if (categorySlideCount > 0 && $emptyFlag.length > 0){ + $emptyFlag.remove(); + } + }); + }, + + _getSlides: function (){ + var categories = []; + this.$('.o_wslides_js_list_item').each(function (){ + categories.push(parseInt($(this).data('slideId'))); + }); + return categories; + }, + _reorderSlides: function (){ + var self = this; + self._rpc({ + route: '/web/dataset/resequence', + params: { + model: "slide.slide", + ids: self._getSlides() + } + }).then(function (res) { + self._checkForEmptySections(); + }); + }, + + /** + * Change links href to fullscreen mode for SEO. + * + * Specifications demand that links are generated (xml) without the "fullscreen" + * parameter for SEO purposes. + * + * This method then adds the parameter as soon as the page is loaded. + * + * @private + */ + _updateHref: function () { + this.$(".o_wslides_js_slides_list_slide_link").each(function (){ + var href = $(this).attr('href'); + var operator = href.indexOf('?') !== -1 ? '&' : '?'; + $(this).attr('href', href + operator + "fullscreen=1"); + }); + } +}); + +return publicWidget.registry.websiteSlidesCourseSlidesList; + +}); diff --git a/addons/website_slides/static/src/js/slides_course_tag_add.js b/addons/website_slides/static/src/js/slides_course_tag_add.js new file mode 100644 index 00000000..b62e9cf0 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_tag_add.js @@ -0,0 +1,377 @@ +odoo.define('website_slides.channel_tag.add', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); + +var _t = core._t; + +var TagCourseDialog = Dialog.extend({ + template: 'website.slides.tag.add', + events: _.extend({}, Dialog.prototype.events, { + 'change input#tag_id' : '_onChangeTag', + }), + + /** + * @override + * @param {Object} parent + * @param {Object} options holding channelId + * + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t("Add a tag"), + size: 'medium', + buttons: [{ + text: _t("Add"), + classes: 'btn-primary', + click: this._onClickFormSubmit.bind(this) + }, { + text: _t("Discard"), + click: this._onClickClose.bind(this) + }] + }); + + this.channelID = parseInt(options.channelId, 10); + this.tagIds = options.channelTagIds || []; + // Open with a tag name as default + this.defaultTag = options.defaultTag; + this._super(parent, options); + }, + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._bindSelect2Dropdown(); + self._hideTagGroup(); + if (self.defaultTag) { + self._setDefaultSelection(); + } + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * 'Tag' and 'Tag Group' management for select2 + * + * @private + */ + _bindSelect2Dropdown: function () { + var self = this; + this.$('#tag_id').select2(this._select2Wrapper(_t('Tag'), + function () { + return self._rpc({ + route: '/slides/channel/tag/search_read', + params: { + fields: ['name'], + domain: [['id','not in',self.tagIds]], + } + }); + }) + ); + this.$('#tag_group_id').select2(this._select2Wrapper(_t('Tag Group (required for new tags)'), + function () { + return self._rpc({ + route: '/slides/channel/tag/group/search_read', + params: { + fields: ['name'], + domain: [], + } + }); + }) + ); + }, + + /** + * Wrapper for select2 load data from server at once and store it. + * + * @private + * @param {String} Placeholder for element. + * @param {Function} Function to fetch data from remote location should return a Promise + * resolved data should be array of object with id and name. eg. [{'id': id, 'name': 'text'}, ...] + * @param {String} [nameKey='name'] (optional) the name key of the returned record + * ('name' if not provided) + * @returns {Object} select2 wrapper object + */ + _select2Wrapper: function (tag, fetchFNC, nameKey) { + nameKey = nameKey || 'name'; + + var values = { + width: '100%', + placeholder: tag, + allowClear: true, + formatNoMatches: false, + selection_data: false, + fetch_rpc_fnc: fetchFNC, + formatSelection: function (data) { + if (data.tag) { + data.text = data.tag; + } + return data.text; + }, + createSearchChoice: function (term, data) { + var addedTags = $(this.opts.element).select2('data'); + if (_.filter(_.union(addedTags, data), function (tag) { + return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0; + }).length === 0) { + if (this.opts.can_create) { + return { + id: _.uniqueId('tag_'), + create: true, + tag: term, + text: _.str.sprintf(_t("Create new %s '%s'"), tag, term), + }; + } else { + return undefined; + } + } + }, + fill_data: function (query, data) { + var that = this, + tags = {results: []}; + _.each(data, function (obj) { + if (that.matcher(query.term, obj[nameKey])) { + tags.results.push({id: obj.id, text: obj[nameKey]}); + } + }); + query.callback(tags); + }, + query: function (query) { + var that = this; + // fetch data only once and store it + if (!this.selection_data) { + this.fetch_rpc_fnc().then(function (data) { + that.can_create = data.can_create; + that.fill_data(query, data.read_results); + that.selection_data = data.read_results; + }); + } else { + this.fill_data(query, this.selection_data); + } + } + }; + return values; + }, + + _setDefaultSelection: function () { + this.$('#tag_id').select2('data', {id: _.uniqueId('tag_'), text: this.defaultTag, create: true}, true); + this.$('#tag_id').select2('readonly', true); + }, + + /** + * Get value for tag_id and [when appropriate] tag_group_id to send to server + * + * @private + */ + _getSelect2DropdownValues: function () { + var result = {}; + var tag = this.$('#tag_id').select2('data'); + if (tag) { + if (tag.create) { + // new tag + var group = this.$('#tag_group_id').select2('data'); + if(group) { + result['tag_id'] = [0, {'name': tag.text}] + if (group.create) { + // new tag group + result['group_id'] = [0, {'name': group.text}]; + } else { + result['group_id'] = [group.id]; + } + } + } else { + result['tag_id'] = [tag.id]; + } + } + return result; + }, + + /** + * Select2 fields makes the "required" input hidden on the interface. + * Therefore we need to make a method to visually provide this requirement + * feedback to users. "tag group" field should only need this when a new tag + * is created. + * + * @private + */ + _formValidate: function ($form) { + $form.addClass('was-validated'); + var result = $form[0].checkValidity(); + + var $tagInput = this.$('#tag_id'); + if ($tagInput.length !== 0){ + var $tagSelect2Container = $tagInput + .closest('.form-group') + .find('.select2-container'); + $tagSelect2Container.removeClass('is-invalid is-valid'); + if ($tagInput.is(':invalid')) { + $tagSelect2Container.addClass('is-invalid'); + } else if ($tagInput.is(':valid')) { + $tagSelect2Container.addClass('is-valid'); + var $tagGroupInput = this.$('#tag_group_id'); + if ($tagGroupInput.length !== 0){ + var $tagGroupSelect2Container = $tagGroupInput + .closest('.form-group') + .find('.select2-container'); + if ($tagGroupInput.is(':invalid')) { + $tagGroupSelect2Container.addClass('is-invalid'); + } else if ($tagGroupInput.is(':valid')) { + $tagGroupSelect2Container.addClass('is-valid'); + } + } + } + } + return result; + }, + + _alertDisplay: function (message) { + this._alertRemove(); + $('<div/>', { + "class": 'alert alert-warning', + role: 'alert' + }).text(message).insertBefore(this.$('form')); + }, + _alertRemove: function () { + this.$('.alert-warning').remove(); + }, + + /** + * When the user IS NOT creating a new tag, this function hides the group tag field + * and makes it not required. Since the select2 field makes an extra container, this + * needs to be hidden along with the group tag input field and its label. + * + * @private + */ + _hideTagGroup: function () { + var $tag_group_id = this.$('#tag_group_id'); + var $tagGroupSelect2Container = $tag_group_id.closest('.form-group'); + $tagGroupSelect2Container.hide(); + $tag_group_id.removeAttr("required"); + $tag_group_id.select2("val", ""); + }, + + /** + * When the user IS creating a new tag, this function shows the field and + * makes it required. Since the select2 field makes an extra container, this + * needs to be shown along with the group input field and its label. + * + * @private + */ + _showTagGroup: function () { + var $tag_group_id = this.$('#tag_group_id'); + var $tagGroupSelect2Container = $tag_group_id.closest('.form-group'); + $tagGroupSelect2Container.show(); + $tag_group_id.attr("required", "required"); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + _onClickFormSubmit: function () { + if (this.defaultTag && !this.channelID) { + this._createNewTag(); + } else { + this._addTagToChannel(); + } + }, + + _addTagToChannel: function () { + var self = this; + var $form = this.$('#slides_channel_tag_add_form'); + if (this._formValidate($form)) { + var values = this._getSelect2DropdownValues(); + return this._rpc({ + route: '/slides/channel/tag/add', + params: {'channel_id': this.channelID, + 'tag_id': values.tag_id, + 'group_id': values.group_id}, + }).then(function (data) { + if (data.error) { + self._alertDisplay(data.error); + } else { + window.location = data.url; + } + }); + } + }, + + _createNewTag: function () { + var self = this; + var $form = this.$('#slides_channel_tag_add_form'); + this.$('#tag_id').select2('readonly', false); + var valid = this._formValidate($form); + this.$('#tag_id').select2('readonly', true); + if (valid) { + var values = this._getSelect2DropdownValues(); + return this._rpc({ + route: '/slide_channel_tag/add', + params: { + 'tag_id': values.tag_id, + 'group_id': values.group_id + }, + }).then(function (data) { + self.trigger_up('tag_refresh', { tag_id: data.tag_id }); + self.close(); + }); + } + }, + + _onClickClose: function () { + if (this.defaultTag && !this.channelID) { + this.trigger_up('tag_remove_new'); + } + this.close(); + }, + + _onChangeTag: function (ev) { + var self = this; + var tag = $(ev.currentTarget).select2('data'); + if (tag && tag.create) { + self._showTagGroup(); + } else { + self._hideTagGroup(); + } + }, +}); + +publicWidget.registry.websiteSlidesTag = publicWidget.Widget.extend({ + selector: '.o_wslides_js_channel_tag_add', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_channel_tag.xml'], + events: { + 'click': '_onAddTagClick', + }, + + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function ($element) { + var data = $element.data(); + return new TagCourseDialog(this, data).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onAddTagClick: function (ev) { + ev.preventDefault(); + this._openDialog($(ev.currentTarget)); + }, +}); + +return { + TagCourseDialog: TagCourseDialog, + websiteSlidesTag: publicWidget.registry.websiteSlidesTag +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_course_unsubscribe.js b/addons/website_slides/static/src/js/slides_course_unsubscribe.js new file mode 100644 index 00000000..ec0e1049 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_unsubscribe.js @@ -0,0 +1,168 @@ +odoo.define('website_slides.unsubscribe_modal', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); +var utils = require('web.utils'); + +var QWeb = core.qweb; +var _t = core._t; + +var SlideUnsubscribeDialog = Dialog.extend({ + template: 'slides.course.unsubscribe.modal', + _texts: { + titleSubscribe: _t("Subscribe"), + titleUnsubscribe: _t("Notifications"), + titleLeaveCourse: _t("Leave the course") + }, + + /** + * @override + * @param {Object} parent + * @param {Object} options + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: options.isFollower === 'True' ? this._texts.titleSubscribe : this._texts.titleUnsubscribe, + size: 'medium', + }); + this._super(parent, options); + + this.set('state', '_subscription'); + this.on('change:state', this, this._onChangeType); + + this.channelID = parseInt(options.channelId, 10); + this.isFollower = options.isFollower === 'True'; + this.enroll = options.enroll; + }, + /** + * @override + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$('input#subscribed').prop('checked', self.isFollower); + self._resetModal(); + }); + }, + + getSubscriptionState: function () { + return this.$('input#subscribed').prop('checked'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + _getModalButtons: function () { + var btnList = []; + var state = this.get('state'); + if (state === '_subscription') { + btnList.push({text: _t("Save"), classes: "btn-primary", click: this._onClickSubscriptionSubmit.bind(this)}); + btnList.push({text: _t("Discard"), close: true}); + btnList.push({text: _t("or Leave the course"), classes: "btn-danger ml-auto", click: this._onClickLeaveCourse.bind(this)}); + } else if (state === '_leave') { + btnList.push({text: _t("Leave the course"), classes: "btn-danger", click: this._onClickLeaveCourseSubmit.bind(this)}); + btnList.push({text: _t("Discard"), click: this._onClickLeaveCourseCancel.bind(this)}); + } + return btnList; + }, + + /** + * @private + */ + _resetModal: function () { + var state = this.get('state'); + if (state === '_subscription') { + this.set_title(this.isFollower ? this._texts.titleUnsubscribe : this._texts.titleSubscribe); + this.$('input#subscribed').prop('checked', this.isFollower); + } + else if (state === '_leave') { + this.set_title(this._texts.titleLeaveCourse); + } + this.set_buttons(this._getModalButtons()); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + _onClickLeaveCourse: function () { + this.set('state', '_leave'); + }, + + _onClickLeaveCourseCancel: function () { + this.set('state', '_subscription'); + }, + + _onClickLeaveCourseSubmit: function () { + this._rpc({ + route: '/slides/channel/leave', + params: {channel_id: this.channelID}, + }).then(function () { + window.location.reload(); + }); + }, + + _onClickSubscriptionSubmit: function () { + if (this.isFollower === this.getSubscriptionState()) { + this.destroy(); + return; + } + this._rpc({ + route: this.getSubscriptionState() ? '/slides/channel/subscribe' : '/slides/channel/unsubscribe', + params: {channel_id: this.channelID}, + }).then(function () { + window.location.reload(); + }); + }, + + _onChangeType: function () { + var currentType = this.get('state'); + var tmpl; + if (currentType === '_subscription') { + tmpl = 'slides.course.unsubscribe.modal.subscription'; + } else if (currentType === '_leave') { + tmpl = 'slides.course.unsubscribe.modal.leave'; + } + this.$('.o_w_slide_unsubscribe_modal_container').empty(); + this.$('.o_w_slide_unsubscribe_modal_container').append(QWeb.render(tmpl, {widget: this})); + + this._resetModal(); + }, +}); + +publicWidget.registry.websiteSlidesUnsubscribe = publicWidget.Widget.extend({ + selector: '.o_wslides_js_channel_unsubscribe', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_unsubscribe.xml'], + events: { + 'click': '_onUnsubscribeClick', + }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function ($element) { + var data = $element.data(); + return new SlideUnsubscribeDialog(this, data).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onUnsubscribeClick: function (ev) { + ev.preventDefault(); + this._openDialog($(ev.currentTarget)); + }, +}); + +return { + SlideUnsubscribeDialog: SlideUnsubscribeDialog, + websiteSlidesUnsubscribe: publicWidget.registry.websiteSlidesUnsubscribe +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_embed.js b/addons/website_slides/static/src/js/slides_embed.js new file mode 100644 index 00000000..f7dfef25 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_embed.js @@ -0,0 +1,250 @@ +/** + * This is a minimal version of the PDFViewer widget. + * It is NOT use in the website_slides module, but it is called when embedding + * a slide/video/document. This code can depend on pdf.js, JQuery and Bootstrap + * (see website_slides.slide_embed_assets bundle, in website_slides_embed.xml) + */ +$(function () { + + if ($('#PDFViewer') && $('#PDFViewerCanvas')) { // check if presentation only + var MIN_ZOOM=1, MAX_ZOOM=10, ZOOM_INCREMENT=.5; + + // define embedded viewer (minimal object of the website.slide.PDFViewer widget) + var EmbeddedViewer = function ($viewer) { + var self = this; + this.viewer = $viewer; + this.slide_url = $viewer.find('#PDFSlideViewer').data('slideurl'); + this.slide_id = $viewer.find('#PDFSlideViewer').data('slideid'); + this.defaultpage = parseInt($viewer.find('#PDFSlideViewer').data('defaultpage')); + this.canvas = $viewer.find('canvas')[0]; + + this.pdf_viewer = new PDFSlidesViewer(this.slide_url, this.canvas, true); + this.pdf_viewer.loadDocument().then(function () { + self.on_loaded_file(); + }); + }; + EmbeddedViewer.prototype.__proto__ = { + // jquery inside the object (like Widget) + $: function (selector) { + return this.viewer.find($(selector)); + }, + // post process action (called in '.then()') + on_loaded_file: function () { + this.$('canvas').show(); + this.$('#page_count').text(this.pdf_viewer.pdf_page_total); + this.$('#PDFViewerLoader').hide(); + if (this.pdf_viewer.pdf_page_total > 1) { + this.$('.o_slide_navigation_buttons').removeClass('hide'); + } + // init first page to display + var initpage = this.defaultpage; + var pageNum = (initpage > 0 && initpage <= this.pdf_viewer.pdf_page_total) ? initpage : 1; + this.render_page(pageNum); + }, + on_rendered_page: function (pageNumber) { + if (pageNumber) { + this.$('#page_number').val(pageNumber); + this.navUpdate(pageNumber); + } + }, + on_resize: function() { + this.render_page(this.pdf_viewer.pdf_page_current); + }, + // page switching + render_page: function (pageNumber) { + this.pdf_viewer.queueRenderPage(pageNumber).then(this.on_rendered_page.bind(this)); + this.navUpdate(pageNumber); + }, + change_page: function () { + var pageAsked = parseInt(this.$('#page_number').val(), 10); + if (1 <= pageAsked && pageAsked <= this.pdf_viewer.pdf_page_total) { + this.pdf_viewer.changePage(pageAsked).then(this.on_rendered_page.bind(this)); + this.navUpdate(pageAsked); + } else { + // if page number out of range, reset the page_counter to the actual page + this.$('#page_number').val(this.pdf_viewer.pdf_page_current); + } + }, + next: function () { + var self = this; + this.pdf_viewer.nextPage().then(function (pageNum) { + if (pageNum) { + self.on_rendered_page(pageNum); + } else { + if (self.pdf_viewer.pdf) { // avoid display suggestion when pdf is not loaded yet + self.display_suggested_slides(); + } + } + }); + }, + previous: function () { + var self = this; + this.pdf_viewer.previousPage().then(function (pageNum) { + if (pageNum) { + self.on_rendered_page(pageNum); + } + self.$("#slide_suggest").addClass('d-none'); + }); + }, + first: function () { + var self = this; + this.pdf_viewer.firstPage().then(function (pageNum) { + self.on_rendered_page(pageNum); + self.$("#slide_suggest").addClass('d-none'); + }); + }, + last: function () { + var self = this; + this.pdf_viewer.lastPage().then(function (pageNum) { + self.on_rendered_page(pageNum); + self.$("#slide_suggest").addClass('d-none'); + }); + }, + zoomIn: function() { + if(this.pdf_viewer.pdf_zoom < MAX_ZOOM) { + this.pdf_viewer.pdf_zoom += ZOOM_INCREMENT; + this.render_page(this.pdf_viewer.pdf_page_current); + } + }, + zoomOut: function() { + if(this.pdf_viewer.pdf_zoom > MIN_ZOOM) { + this.pdf_viewer.pdf_zoom -= ZOOM_INCREMENT; + this.render_page(this.pdf_viewer.pdf_page_current); + } + }, + navUpdate: function (pageNum) { + this.$('#first').toggleClass('disabled', pageNum < 3 ); + this.$('#previous').toggleClass('disabled', pageNum < 2 ); + this.$('#next, #last').removeClass('disabled'); + this.$('#zoomout').toggleClass('disabled', this.pdf_viewer.pdf_zoom <= MIN_ZOOM); + this.$('#zoomin').toggleClass('disabled', this.pdf_viewer.pdf_zoom >= MAX_ZOOM); + }, + // full screen mode + fullscreen: function () { + this.pdf_viewer.toggleFullScreen(); + }, + fullScreenFooter: function (ev) { + if (ev.target.id === "PDFViewerCanvas") { + this.pdf_viewer.toggleFullScreenFooter(); + } + }, + // display suggestion displayed after last slide + display_suggested_slides: function () { + this.$("#slide_suggest").removeClass('d-none'); + this.$('#next, #last').addClass('disabled'); + }, + }; + + // embedded pdf viewer + var embeddedViewer = new EmbeddedViewer($('#PDFViewer')); + + // bind the actions + $('#previous').on('click', function () { + embeddedViewer.previous(); + }); + $('#next').on('click', function () { + embeddedViewer.next(); + }); + $('#first').on('click', function () { + embeddedViewer.first(); + }); + $('#last').on('click', function () { + embeddedViewer.last(); + }); + $('#zoomin').on('click', function () { + embeddedViewer.zoomIn(); + }); + $('#zoomout').on('click', function () { + embeddedViewer.zoomOut(); + }); + $('#page_number').on('change', function () { + embeddedViewer.change_page(); + }); + $('#fullscreen').on('click', function () { + embeddedViewer.fullscreen(); + }); + $('#PDFViewer').on('click', function (ev) { + embeddedViewer.fullScreenFooter(ev); + }); + $('#PDFViewer').on('wheel', function (ev) { + if (ev.metaKey || ev.ctrlKey) { + if (ev.originalEvent.deltaY > 0) { + embeddedViewer.zoomOut(); + } else if(ev.originalEvent.deltaY < 0) { + embeddedViewer.zoomIn(); + } + return false; + } + }); + $(window).on('resize', _.debounce(function() { + embeddedViewer.on_resize(); + }, 500)); + + // switching slide with keyboard + $(document).keydown(function (ev) { + if (ev.keyCode === 37 || ev.keyCode === 38) { + embeddedViewer.previous(); + } + if (ev.keyCode === 39 || ev.keyCode === 40) { + embeddedViewer.next(); + } + }); + + // display the option panels + $('.oe_slide_js_embed_option_link').on('click', function (ev) { + ev.preventDefault(); + var toggleDiv = $(this).data('slide-option-id'); + $('.oe_slide_embed_option').not(toggleDiv).each(function () { + $(this).hide(); + }); + $(toggleDiv).slideToggle(); + }); + + // animation for the suggested slides + $('.oe_slides_suggestion_media').hover( + function () { + $(this).find('.oe_slides_suggestion_caption').stop().slideDown(250); + }, + function () { + $(this).find('.oe_slides_suggestion_caption').stop().slideUp(250); + } + ); + + // embed widget page selector + $('.oe_slide_js_embed_code_widget input').on('change', function () { + var page = parseInt($(this).val()); + if (!(page > 0 && page <= embeddedViewer.pdf_viewer.pdf_page_total)) { + page = 1; + } + var actualCode = embeddedViewer.$('.slide_embed_code').val(); + var newCode = actualCode.replace(/(page=).*?([^\d]+)/, '$1' + page + '$2'); + embeddedViewer.$('.slide_embed_code').val(newCode); + }); + + // To avoid create a dependancy to openerpframework.js, we use JQuery AJAX to post data instead of ajax.jsonRpc + $('.oe_slide_js_share_email button').on('click', function () { + var widget = $('.oe_slide_js_share_email'); + var input = widget.find('input'); + var slideID = widget.find('button').data('slide-id'); + if (input.val() && input[0].checkValidity()) { + widget.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + $.ajax({ + type: "POST", + dataType: 'json', + url: '/slides/slide/send_share_email', + contentType: "application/json; charset=utf-8", + data: JSON.stringify({'jsonrpc': "2.0", 'method': "call", "params": {'slide_id': slideID, 'email': input.val()}}), + success: function () { + widget.html($('<div class="alert alert-info" role="alert"><strong>Thank you!</strong> Mail has been sent.</div>')); + }, + error: function (data) { + console.error("ERROR ", data); + }, + }); + } else { + widget.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + input.focus(); + } + }); + } +}); diff --git a/addons/website_slides/static/src/js/slides_share.js b/addons/website_slides/static/src/js/slides_share.js new file mode 100644 index 00000000..c9942f79 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_share.js @@ -0,0 +1,105 @@ +odoo.define('website_slides.slides_share', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +require('website_slides.slides'); +var core = require('web.core'); +var _t = core._t; + +var ShareMail = publicWidget.Widget.extend({ + events: { + 'click button': '_sendMail', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _sendMail: function () { + var self = this; + var input = this.$('input'); + var slideID = this.$('button').data('slide-id'); + if (input.val() && input[0].checkValidity()) { + this.$el.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + this._rpc({ + route: '/slides/slide/send_share_email', + params: { + slide_id: slideID, + email: input.val(), + }, + }).then(function () { + self.$el.html($('<div class="alert alert-info" role="alert">' + _t('<strong>Thank you!</strong> Mail has been sent.') + '</div>')); + }); + } else { + this.$el.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + input.focus(); + } + }, +}); + +publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({ + selector: '#wrapwrap', + events: { + 'click a.o_wslides_js_social_share': '_onSlidesSocialShare', + 'click .o_clipboard_button': '_onShareLinkCopy', + }, + + /** + * @override + * @param {Object} parent + */ + start: function (parent) { + var defs = [this._super.apply(this, arguments)]; + defs.push(new ShareMail(this).attachTo($('.oe_slide_js_share_email'))); + + return Promise.all(defs); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @param {Object} ev + */ + _onSlidesSocialShare: function (ev) { + ev.preventDefault(); + var popUpURL = $(ev.currentTarget).attr('href'); + var popUp = window.open(popUpURL, 'Share Dialog', 'width=626,height=436'); + $(window).on('focus', function () { + if (popUp.closed) { + $(window).off('focus'); + } + }); + }, + + _onShareLinkCopy: function (ev) { + ev.preventDefault(); + var $clipboardBtn = $(ev.currentTarget); + $clipboardBtn.tooltip({title: "Copied !", trigger: "manual", placement: "bottom"}); + var self = this; + var clipboard = new ClipboardJS('#' + $clipboardBtn[0].id, { + target: function () { + var share_link_el = self.$('#wslides_share_link_id_' + $clipboardBtn[0].id.split('id_')[1]); + return share_link_el[0]; + }, + container: this.el + }); + clipboard.on('success', function () { + clipboard.destroy(); + $clipboardBtn.tooltip('show'); + _.delay(function () { + $clipboardBtn.tooltip("hide"); + }, 800); + }); + clipboard.on('error', function (e) { + console.log(e); + clipboard.destroy(); + }) + }, +}); +}); diff --git a/addons/website_slides/static/src/js/slides_slide_archive.js b/addons/website_slides/static/src/js/slides_slide_archive.js new file mode 100644 index 00000000..cccf2e0b --- /dev/null +++ b/addons/website_slides/static/src/js/slides_slide_archive.js @@ -0,0 +1,108 @@ +odoo.define('website_slides.slide.archive', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var Dialog = require('web.Dialog'); +var core = require('web.core'); +var _t = core._t; + +var SlideArchiveDialog = Dialog.extend({ + template: 'slides.slide.archive', + + /** + * @override + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t('Archive Slide'), + size: 'medium', + buttons: [{ + text: _t('Archive'), + classes: 'btn-primary', + click: this._onClickArchive.bind(this) + }, { + text: _t('Cancel'), + close: true + }] + }); + + this.$slideTarget = options.slideTarget; + this.slideId = this.$slideTarget.data('slideId'); + this._super(parent, options); + }, + _checkForEmptySections: function (){ + $('.o_wslides_slide_list_category').each(function (){ + var $categoryHeader = $(this).find('.o_wslides_slide_list_category_header'); + var categorySlideCount = $(this).find('.o_wslides_slides_list_slide:not(.o_not_editable)').length; + var $emptyFlagContainer = $categoryHeader.find('.o_wslides_slides_list_drag').first(); + var $emptyFlag = $emptyFlagContainer.find('small'); + if (categorySlideCount === 0 && $emptyFlag.length === 0){ + $emptyFlagContainer.append($('<small>', { + 'class': "ml-1 text-muted font-weight-bold", + text: _t("(empty)") + })); + } + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Calls 'archive' on slide controller and then visually removes the slide dom element + */ + _onClickArchive: function () { + var self = this; + + this._rpc({ + route: '/slides/slide/archive', + params: { + slide_id: this.slideId + }, + }).then(function (isArchived) { + if (isArchived){ + self.$slideTarget.closest('.o_wslides_slides_list_slide').remove(); + self._checkForEmptySections(); + } + self.close(); + }); + } +}); + +publicWidget.registry.websiteSlidesSlideArchive = publicWidget.Widget.extend({ + selector: '.o_wslides_js_slide_archive', + xmlDependencies: ['/website_slides/static/src/xml/slide_management.xml'], + events: { + 'click': '_onArchiveSlideClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function ($slideTarget) { + new SlideArchiveDialog(this, {slideTarget: $slideTarget}).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onArchiveSlideClick: function (ev) { + ev.preventDefault(); + var $slideTarget = $(ev.currentTarget); + this._openDialog($slideTarget); + }, +}); + +return { + slideArchiveDialog: SlideArchiveDialog, + websiteSlidesSlideArchive: publicWidget.registry.websiteSlidesSlideArchive +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_slide_like.js b/addons/website_slides/static/src/js/slides_slide_like.js new file mode 100644 index 00000000..88bc3db1 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_slide_like.js @@ -0,0 +1,114 @@ +odoo.define('website_slides.slides.slide.like', function (require) { +'use strict'; + +var core = require('web.core'); +var publicWidget = require('web.public.widget'); +require('website_slides.slides'); + +var _t = core._t; + +var SlideLikeWidget = publicWidget.Widget.extend({ + events: { + 'click .o_wslides_js_slide_like_up': '_onClickUp', + 'click .o_wslides_js_slide_like_down': '_onClickDown', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} $el + * @param {String} message + */ + _popoverAlert: function ($el, message) { + $el.popover({ + trigger: 'focus', + placement: 'bottom', + container: 'body', + html: true, + content: function () { + return message; + } + }).popover('show'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClick: function (slideId, voteType) { + var self = this; + this._rpc({ + route: '/slides/slide/like', + params: { + slide_id: slideId, + upvote: voteType === 'like', + }, + }).then(function (data) { + if (! data.error) { + self.$el.find('span.o_wslides_js_slide_like_up span').text(data.likes); + self.$el.find('span.o_wslides_js_slide_like_down span').text(data.dislikes); + } else { + if (data.error === 'public_user') { + var message = _t('Please <a href="/web/login?redirect=%s">login</a> to vote this lesson'); + var signupAllowed = data.error_signup_allowed || false; + if (signupAllowed) { + message = _t('Please <a href="/web/signup?redirect=%s">create an account</a> to vote this lesson'); + } + self._popoverAlert(self.$el, _.str.sprintf(message, (document.URL))); + } else if (data.error === 'vote_done') { + self._popoverAlert(self.$el, _t('You have already voted for this lesson')); + } else if (data.error === 'slide_access') { + self._popoverAlert(self.$el, _t('You don\'t have access to this lesson')); + } else if (data.error === 'channel_membership_required') { + self._popoverAlert(self.$el, _t('You must be member of this course to vote')); + } else if (data.error === 'channel_comment_disabled') { + self._popoverAlert(self.$el, _t('Votes and comments are disabled for this course')); + } else if (data.error === 'channel_karma_required') { + self._popoverAlert(self.$el, _t('You don\'t have enough karma to vote')); + } else { + self._popoverAlert(self.$el, _t('Unknown error')); + } + } + }); + }, + + _onClickUp: function (ev) { + var slideId = $(ev.currentTarget).data('slide-id'); + return this._onClick(slideId, 'like'); + }, + + _onClickDown: function (ev) { + var slideId = $(ev.currentTarget).data('slide-id'); + return this._onClick(slideId, 'dislike'); + }, +}); + +publicWidget.registry.websiteSlidesSlideLike = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + /** + * @override + * @param {Object} parent + */ + start: function () { + var self = this; + var defs = [this._super.apply(this, arguments)]; + $('.o_wslides_js_slide_like').each(function () { + defs.push(new SlideLikeWidget(self).attachTo($(this))); + }); + return Promise.all(defs); + }, +}); + +return { + slideLikeWidget: SlideLikeWidget, + websiteSlidesSlideLike: publicWidget.registry.websiteSlidesSlideLike +}; + +}); diff --git a/addons/website_slides/static/src/js/slides_slide_toggle_is_preview.js b/addons/website_slides/static/src/js/slides_slide_toggle_is_preview.js new file mode 100644 index 00000000..604170b6 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_slide_toggle_is_preview.js @@ -0,0 +1,40 @@ +odoo.define('website_slides.slide.preview', function (require) { + 'use strict'; + + var publicWidget = require('web.public.widget'); + + publicWidget.registry.websiteSlidesSlideToggleIsPreview = publicWidget.Widget.extend({ + selector: '.o_wslides_js_slide_toggle_is_preview', + xmlDependencies: ['/website_slides/static/src/xml/slide_management.xml'], + events: { + 'click': '_onPreviewSlideClick', + }, + + _toggleSlidePreview: function($slideTarget) { + this._rpc({ + route: '/slides/slide/toggle_is_preview', + params: { + slide_id: $slideTarget.data('slideId') + }, + }).then(function (isPreview) { + if (isPreview) { + $slideTarget.removeClass('badge-light badge-hide border'); + $slideTarget.addClass('badge-success'); + } else { + $slideTarget.removeClass('badge-success'); + $slideTarget.addClass('badge-light badge-hide border'); + } + }); + }, + + _onPreviewSlideClick: function (ev) { + ev.preventDefault(); + this._toggleSlidePreview($(ev.currentTarget)); + }, + }); + + return { + websiteSlidesSlideToggleIsPreview: publicWidget.registry.websiteSlidesSlideToggleIsPreview + }; + +}); diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js new file mode 100644 index 00000000..3ab24914 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -0,0 +1,678 @@ +odoo.define('website_slides.upload_modal', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); +var utils = require('web.utils'); + +var QWeb = core.qweb; +var _t = core._t; + +var SlideUploadDialog = Dialog.extend({ + template: 'website.slide.upload.modal', + events: _.extend({}, Dialog.prototype.events, { + 'click .o_wslides_js_upload_install_button': '_onClickInstallModule', + 'click .o_wslides_select_type': '_onClickSlideTypeIcon', + 'change input#upload': '_onChangeSlideUpload', + 'change input#url': '_onChangeSlideUrl', + }), + + /** + * @override + * @param {Object} parent + * @param {Object} options holding channelId and optionally upload and publish control parameters + * @param {Object} options.modulesToInstall: list of additional modules to + * install {id: module ID, name: module short description} + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t("Upload a document"), + size: 'medium', + }); + this._super(parent, options); + this._setup(); + + this.channelID = parseInt(options.channelId, 10); + this.defaultCategoryID = parseInt(options.categoryId,10); + this.canUpload = options.canUpload === 'True'; + this.canPublish = options.canPublish === 'True'; + this.modulesToInstall = options.modulesToInstall ? JSON.parse(options.modulesToInstall.replace(/'/g, '"')) : null; + this.modulesToInstallStatus = null; + + this.set('state', '_select'); + this.on('change:state', this, this._onChangeType); + this.set('can_submit_form', false); + this.on('change:can_submit_form', this, this._onChangeCanSubmitForm); + + this.file = {}; + this.isValidUrl = true; + }, + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._resetModalButton(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} message + */ + _alertDisplay: function (message) { + this._alertRemove(); + $('<div/>', { + "class": 'alert alert-warning', + id: 'upload-alert', + role: 'alert' + }).text(message).insertBefore(this.$('form')); + }, + _alertRemove: function () { + this.$('#upload-alert').remove(); + }, + /** + * Section and tags management from select2 + * + * @private + */ + _bindSelect2Dropdown: function () { + var self = this; + this.$('#category_id').select2(this._select2Wrapper(_t('Section'), false, + function () { + return self._rpc({ + route: '/slides/category/search_read', + params: { + fields: ['name'], + domain: [['channel_id', '=', self.channelID]], + } + }); + }) + ); + this.$('#tag_ids').select2(this._select2Wrapper(_t('Tags'), true, function () { + return self._rpc({ + route: '/slides/tag/search_read', + params: { + fields: ['name'], + domain: [], + } + }); + })); + }, + _fetchUrlPreview: function (url) { + return this._rpc({ + route: '/slides/prepare_preview/', + params: { + 'url': url, + 'channel_id': this.channelID + }, + }); + }, + _formSetFieldValue: function (fieldId, value) { + this.$('form').find('#'+fieldId).val(value); + }, + _formGetFieldValue: function (fieldId) { + return this.$('#'+fieldId).val(); + }, + _formValidate: function () { + var form = this.$("form"); + form.addClass('was-validated'); + return form[0].checkValidity() && this.isValidUrl; + }, + /** + * Extract values to submit from form, force the slide_type according to + * filled values. + * + * @private + */ + _formValidateGetValues: function (forcePublished) { + var canvas = this.$('#data_canvas')[0]; + var values = _.extend({ + 'channel_id': this.channelID, + 'name': this._formGetFieldValue('name'), + 'url': this._formGetFieldValue('url'), + 'description': this._formGetFieldValue('description'), + 'duration': this._formGetFieldValue('duration'), + 'is_published': forcePublished, + }, this._getSelect2DropdownValues()); // add tags and category + + // default slide_type (for webpage for instance) + if (_.contains(this.slide_type_data), this.get('state')) { + values['slide_type'] = this.get('state'); + } + + if (this.file.type === 'application/pdf') { + _.extend(values, { + 'image_1920': canvas.toDataURL().split(',')[1], + 'slide_type': canvas.height > canvas.width ? 'document' : 'presentation', + 'mime_type': this.file.type, + 'datas': this.file.data + }); + } else if (values['slide_type'] === 'webpage') { + _.extend(values, { + 'mime_type': 'text/html', + 'image_1920': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data, + }); + } else if (/^image\/.*/.test(this.file.type)) { + if (values['slide_type'] === 'presentation') { + _.extend(values, { + 'slide_type': 'infographic', + 'mime_type': this.file.type === 'image/svg+xml' ? 'image/png' : this.file.type, + 'datas': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data + }); + } else { + _.extend(values, { + 'image_1920': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data, + }); + } + } + return values; + }, + /** + * @private + */ + _fileReset: function () { + var control = this.$('#upload'); + control.replaceWith(control = control.clone(true)); + this.file.name = false; + }, + + _getModalButtons: function () { + var btnList = []; + var state = this.get('state'); + if (state === '_select') { + btnList.push({text: _t("Cancel"), classes: 'o_w_slide_cancel', close: true}); + } else if (state === '_import') { + if (! this.modulesToInstallStatus.installing) { + btnList.push({text: this.modulesToInstallStatus.failed ? _t("Retry") : _t("Install"), classes: 'btn-primary', click: this._onClickInstallModuleConfirm.bind(this)}); + } + btnList.push({text: _t("Discard"), classes: 'o_w_slide_go_back', click: this._onClickGoBack.bind(this)}); + } else if (state !== '_upload') { // no button when uploading + if (this.canUpload) { + if (this.canPublish) { + btnList.push({text: _t("Save & Publish"), classes: 'btn-primary o_w_slide_upload o_w_slide_upload_published', click: this._onClickFormSubmit.bind(this)}); + btnList.push({text: _t("Save"), classes: 'o_w_slide_upload', click: this._onClickFormSubmit.bind(this)}); + } else { + btnList.push({text: _t("Save"), classes: 'btn-primary o_w_slide_upload', click: this._onClickFormSubmit.bind(this)}); + } + } + btnList.push({text: _t("Discard"), classes: 'o_w_slide_go_back', click: this._onClickGoBack.bind(this)}); + } + return btnList; + }, + /** + * Get value for category_id and tag_ids (ORM cmd) to send to server + * + * @private + */ + _getSelect2DropdownValues: function () { + var result = {}; + var self = this; + // tags + var tagValues = []; + _.each(this.$('#tag_ids').select2('data'), function (val) { + if (val.create) { + tagValues.push([0, 0, {'name': val.text}]); + } else { + tagValues.push([4, val.id]); + } + }); + if (tagValues) { + result['tag_ids'] = tagValues; + } + // category + if (!self.defaultCategoryID) { + var categoryValue = this.$('#category_id').select2('data'); + if (categoryValue && categoryValue.create) { + result['category_id'] = [0, {'name': categoryValue.text}]; + } else if (categoryValue) { + result['category_id'] = [categoryValue.id]; + this.categoryID = categoryValue.id; + } + } else { + result['category_id'] = [self.defaultCategoryID]; + this.categoryID = self.defaultCategoryID; + } + return result; + }, + /** + * Reset the footer buttons, according to current state of modal + * + * @private + */ + _resetModalButton: function () { + this.set_buttons(this._getModalButtons()); + }, + /** + * Wrapper for select2 load data from server at once and store it. + * + * @private + * @param {String} Placeholder for element. + * @param {bool} true for multiple selection box, false for single selection + * @param {Function} Function to fetch data from remote location should return a Promise + * resolved data should be array of object with id and name. eg. [{'id': id, 'name': 'text'}, ...] + * @param {String} [nameKey='name'] (optional) the name key of the returned record + * ('name' if not provided) + * @returns {Object} select2 wrapper object + */ + _select2Wrapper: function (tag, multi, fetchFNC, nameKey) { + nameKey = nameKey || 'name'; + + var values = { + width: '100%', + placeholder: tag, + allowClear: true, + formatNoMatches: false, + selection_data: false, + fetch_rpc_fnc: fetchFNC, + formatSelection: function (data) { + if (data.tag) { + data.text = data.tag; + } + return data.text; + }, + createSearchChoice: function (term, data) { + var addedTags = $(this.opts.element).select2('data'); + if (_.filter(_.union(addedTags, data), function (tag) { + return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0; + }).length === 0) { + if (this.opts.can_create) { + return { + id: _.uniqueId('tag_'), + create: true, + tag: term, + text: _.str.sprintf(_t("Create new %s '%s'"), tag, term), + }; + } else { + return undefined; + } + } + }, + fill_data: function (query, data) { + var self = this, + tags = {results: []}; + _.each(data, function (obj) { + if (self.matcher(query.term, obj[nameKey])) { + tags.results.push({id: obj.id, text: obj[nameKey]}); + } + }); + query.callback(tags); + }, + query: function (query) { + var self = this; + // fetch data only once and store it + if (!this.selection_data) { + this.fetch_rpc_fnc().then(function (data) { + self.can_create = data.can_create; + self.fill_data(query, data.read_results); + self.selection_data = data.read_results; + }); + } else { + this.fill_data(query, this.selection_data); + } + } + }; + + if (multi) { + values['multiple'] = true; + } + + return values; + }, + /** + * Init the data relative to the support slide type to upload + * + * @private + */ + _setup: function () { + this.slide_type_data = { + presentation: { + icon: 'fa-file-pdf-o', + label: _t('Presentation'), + template: 'website.slide.upload.modal.presentation', + }, + webpage: { + icon: 'fa-file-text', + label: _t('Web Page'), + template: 'website.slide.upload.modal.webpage', + }, + video: { + icon: 'fa-video-camera', + label: _t('Video'), + template: 'website.slide.upload.modal.video', + }, + quiz: { + icon: 'fa-question-circle', + label: _t('Quiz'), + template: 'website.slide.upload.quiz' + } + }; + }, + /** + * Show the preview + * @private + */ + _showPreviewColumn: function () { + this.$('.o_slide_tutorial').addClass('d-none'); + this.$('.o_slide_preview').removeClass('d-none'); + }, + /** + * Hide the preview + * @private + */ + _hidePreviewColumn: function () { + this.$('.o_slide_tutorial').removeClass('d-none'); + this.$('.o_slide_preview').addClass('d-none'); + }, + /** + * @private + */ + // TODO: Remove this part, as now SVG support in image resize tools is included + //Python PIL does not support SVG, so converting SVG to PNG + _svgToPng: function () { + var img = this.$el.find('img#slide-image')[0]; + var canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + return canvas.toDataURL('image/png').split(',')[1]; + }, + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + _onChangeType: function () { + var currentType = this.get('state'); + var tmpl; + this.$modal.find('.modal-dialog').removeClass('modal-lg'); + if (currentType === '_select') { + tmpl = 'website.slide.upload.modal.select'; + } else if (currentType === '_upload') { + tmpl = 'website.slide.upload.modal.uploading'; + } else if (currentType === '_import') { + tmpl = 'website.slide.upload.modal.import'; + } else { + tmpl = this.slide_type_data[currentType]['template']; + this.$modal.find('.modal-dialog').addClass('modal-lg'); + } + this.$('.o_w_slide_upload_modal_container').empty(); + this.$('.o_w_slide_upload_modal_container').append(QWeb.render(tmpl, {widget: this})); + + this._resetModalButton(); + + if (currentType === '_import') { + this.set_title(_t("New Certification")); + } else { + this.set_title(_t("Upload a document")); + } + }, + _onChangeCanSubmitForm: function (ev) { + if (this.get('can_submit_form')) { + this.$('.o_w_slide_upload').button('reset'); + } else { + this.$('.o_w_slide_upload').button('loading'); + } + }, + _onChangeSlideUpload: function (ev) { + var self = this; + this._alertRemove(); + + var $input = $(ev.currentTarget); + var preventOnchange = $input.data('preventOnchange'); + var $preview = self.$('#slide-image'); + + var file = ev.target.files[0]; + if (!file) { + this.$('#slide-image').attr('src', '/website_slides/static/src/img/document.png'); + this._hidePreviewColumn(); + return; + } + var isImage = /^image\/.*/.test(file.type); + var loaded = false; + this.file.name = file.name; + this.file.type = file.type; + if (!(isImage || this.file.type === 'application/pdf')) { + this._alertDisplay(_t("Invalid file type. Please select pdf or image file")); + this._fileReset(); + this._hidePreviewColumn(); + return; + } + if (file.size / 1024 / 1024 > 25) { + this._alertDisplay(_t("File is too big. File size cannot exceed 25MB")); + this._fileReset(); + this._hidePreviewColumn(); + return; + } + + utils.getDataURLFromFile(file).then(function (buffer) { + if (isImage) { + $preview.attr('src', buffer); + } + buffer = buffer.split(',')[1]; + self.file.data = buffer; + self._showPreviewColumn(); + }); + + if (file.type === 'application/pdf') { + var ArrayReader = new FileReader(); + this.set('can_submit_form', false); + // file read as ArrayBuffer for pdfjsLib get_Document API + ArrayReader.readAsArrayBuffer(file); + ArrayReader.onload = function (evt) { + var buffer = evt.target.result; + var passwordNeeded = function () { + self._alertDisplay(_t("You can not upload password protected file.")); + self._fileReset(); + self.set('can_submit_form', true); + }; + /** + * The following line fixes pdfjsLib 'Util' global variable. + * This is (most likely) related to #32181 which lazy loads most assets. + * + * That caused an issue where the global 'Util' variable from pdfjsLib can be + * (depending of which libraries load first) overridden by the global 'Util' + * variable of bootstrap. + * (See 'lib/bootstrap/js/util.js' and 'web/static/lib/pdfjs/build/pdfjs.js') + * + * This commit ensures that the global 'Util' variable is set to the one of pdfjsLib + * right before it's used. + * + * Eventually, we should update or get rid of one of the two libraries since they're + * not compatible together, or make a wrapper that makes them compatible. + * In the mean time, this small fix allows not refactoring all of this and can not + * cause much harm. + */ + Util = window.pdfjsLib.Util; + window.pdfjsLib.getDocument(new Uint8Array(buffer), null, passwordNeeded).then(function getPdf(pdf) { + self._formSetFieldValue('duration', (pdf._pdfInfo.numPages || 0) * 5); + pdf.getPage(1).then(function getFirstPage(page) { + var scale = 1; + var viewport = page.getViewport(scale); + var canvas = document.getElementById('data_canvas'); + var context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + // Render PDF page into canvas context + page.render({ + canvasContext: context, + viewport: viewport + }).then(function () { + var imageData = self.$('#data_canvas')[0].toDataURL(); + $preview.attr('src', imageData); + if (loaded) { + self.set('can_submit_form', true); + } + loaded = true; + self._showPreviewColumn(); + }); + }); + }); + }; + } + + if (!preventOnchange) { + var input = file.name; + var inputVal = input.substr(0, input.lastIndexOf('.')) || input; + if (this._formGetFieldValue('name') === "") { + this._formSetFieldValue('name', inputVal); + } + } + }, + _onChangeSlideUrl: function (ev) { + var self = this; + var url = $(ev.target).val(); + this._alertRemove(); + this.isValidUrl = false; + this.set('can_submit_form', false); + this._fetchUrlPreview(url).then(function (data) { + self.set('can_submit_form', true); + if (data.error) { + self._alertDisplay(data.error); + } else { + if (data.completion_time) { + // hours to minutes conversion + self._formSetFieldValue('duration', Math.round(data.completion_time * 60)); + } + self.$('#slide-image').attr('src', data.url_src); + self._formSetFieldValue('name', data.title); + self._formSetFieldValue('description', data.description); + + self.isValidUrl = true; + self._showPreviewColumn(); + } + }); + }, + + _onClickInstallModule: function (ev) { + var $btn = $(ev.currentTarget); + var moduleId = $btn.data('moduleId'); + if (this.modulesToInstallStatus) { + this.set('state', '_import'); + if (this.modulesToInstallStatus.installing) { + this.$('#o_wslides_install_module_text') + .text(_.str.sprintf(_t('Already installing "%s".'), this.modulesToInstallStatus.name)); + } else if (this.modulesToInstallStatus.failed) { + this.$('#o_wslides_install_module_text') + .text(_.str.sprintf(_t('Failed to install "%s".'), this.modulesToInstallStatus.name)); + } + } else { + this.modulesToInstallStatus = _.extend({}, _.find(this.modulesToInstall, function (item) { return item.id === moduleId; })); + this.set('state', '_import'); + this.$('#o_wslides_install_module_text') + .text(_.str.sprintf(_t('Do you want to install the "%s" app?'), this.modulesToInstallStatus.name)); + } + }, + + _onClickInstallModuleConfirm: function () { + var self = this; + var $el = this.$('#o_wslides_install_module_text'); + $el.text(_.str.sprintf(_t('Installing "%s".'), this.modulesToInstallStatus.name)); + this.modulesToInstallStatus.installing = true; + this._resetModalButton(); + this._rpc({ + model: 'ir.module.module', + method: 'button_immediate_install', + args: [[this.modulesToInstallStatus.id]], + }).then(function () { + window.location.href = window.location.origin + window.location.pathname + '?enable_slide_upload'; + }, function () { + $el.text(_.str.sprintf(_t('Failed to install "%s".'), self.modulesToInstallStatus.name)); + self.modulesToInstallStatus.installing = false; + self.modulesToInstallStatus.failed = true; + self._resetModalButton(); + }); + }, + + _onClickGoBack: function () { + this.set('state', '_select'); + this.isValidUrl = true; + if (this.modulesToInstallStatus && !this.modulesToInstallStatus.installing) { + this.modulesToInstallStatus = null; + } + }, + + _onClickFormSubmit: function (ev) { + var self = this; + var $btn = $(ev.currentTarget); + if (this._formValidate()) { + var values = this._formValidateGetValues($btn.hasClass('o_w_slide_upload_published')); // get info before changing state + var oldType = this.get('state'); + this.set('state', '_upload'); + return this._rpc({ + route: '/slides/add_slide', + params: values, + }).then(function (data) { + self._onFormSubmitDone(data, oldType); + }); + } + }, + + _onFormSubmitDone: function (data, oldType) { + if (data.error) { + this.set('state', oldType); + this._alertDisplay(data.error); + } else { + window.location = data.url; + } + }, + + _onClickSlideTypeIcon: function (ev) { + var $elem = this.$(ev.currentTarget); + var slideType = $elem.data('slideType'); + this.set('state', slideType); + + this._bindSelect2Dropdown(); // rebind select2 at each modal body rendering + }, +}); + +publicWidget.registry.websiteSlidesUpload = publicWidget.Widget.extend({ + selector: '.o_wslides_js_slide_upload', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'], + events: { + 'click': '_onUploadClick', + }, + + /** + * @override + */ + start: function () { + // Automatically open the upload dialog if requested from query string + if (this.$el.attr('data-open-modal')) { + this.$el.removeAttr('data-open-modal'); + this._openDialog(this.$el); + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function ($element) { + var data = $element.data(); + return new SlideUploadDialog(this, data).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onUploadClick: function (ev) { + ev.preventDefault(); + this._openDialog($(ev.currentTarget)); + }, +}); + +return { + SlideUploadDialog: SlideUploadDialog, + websiteSlidesUpload: publicWidget.registry.websiteSlidesUpload +}; + +}); diff --git a/addons/website_slides/static/src/js/tours/slides_tour.js b/addons/website_slides/static/src/js/tours/slides_tour.js new file mode 100644 index 00000000..249a92f3 --- /dev/null +++ b/addons/website_slides/static/src/js/tours/slides_tour.js @@ -0,0 +1,117 @@ +odoo.define('website_slides.slides_tour', function (require) { +"use strict"; + +var core = require('web.core'); +var _t = core._t; + +var tour = require('web_tour.tour'); + +tour.register('slides_tour', { + url: '/slides', +}, [{ + trigger: '#new-content-menu > a', + content: _t("Welcome on your course's home page. It's still empty for now. Click on \"<b>New</b>\" to write your first course."), + position: 'bottom', +}, { + trigger: 'a[data-action="new_slide_channel"]', + content: _t("Select <b>Course</b> to create it and manage it."), + position: 'bottom', + width: 210, +}, { + trigger: 'input[name="name"]', + content: _t("Give your course an engaging <b>Title</b>."), + position: 'bottom', + width: 280, + run: 'text My New Course', +}, { + trigger: 'textarea[name="description"]', + content: _t("Give your course a helpful <b>Description</b>."), + position: 'bottom', + width: 300, + run: 'text This course is for advanced users.', +}, { + trigger: 'button.btn-primary', + content: _t("Click on the <b>Create</b> button to create your first course."), +}, { + trigger: '.o_wslides_js_slide_section_add', + content: _t("Congratulations, your course has been created, but there isn't any content yet. First, let's add a <b>Section</b> to give your course a structure."), + position: 'bottom', +}, { + trigger: 'input[name="name"]', + content: _t("A good course has structure and a table of content. Your first section will be the <b>Introduction</b>."), + position: 'bottom', +}, { + trigger: 'button.btn-primary', + content: _t("Click on <b>Save</b> to apply changes."), + position: 'bottom', + width: 260, +}, { + trigger: 'a.btn-primary.o_wslides_js_slide_upload', + content: _t("Your first section is created, now it's time to add lessons to your course. Click on <b>Add Content</b> to upload a document, create a web page or link a video."), + position: 'bottom', +}, { + trigger: 'a[data-slide-type="presentation"]', + content: _t("First, let's add a <b>Presentation</b>. It can be a .pdf or an image."), + position: 'bottom', +}, { + trigger: 'input#upload', + content: _t("Choose a <b>File</b> on your computer."), +}, { + trigger: 'input#name', + content: _t("The <b>Title</b> of your lesson is autocompleted but you can change it if you want.</br>A <b>Preview</b> of your file is available on the right side of the screen."), +}, { + trigger: 'input#duration', + content: _t("The <b>Duration</b> of the lesson is based on the number of pages of your document. You can change this number if your attendees will need more time to assimilate the content."), +}, { + trigger: 'button.o_w_slide_upload_published', + content: _t("<b>Save & Publish</b> your lesson to make it available to your attendees."), + position: 'bottom', + width: 285, +}, { + trigger: 'span.badge-info:contains("New")', + content: _t("Congratulations! Your first lesson is available. Let's see the options available here. The tag \"<b>New</b>\" indicates that this lesson was created less than 7 days ago."), + position: 'bottom', +}, { + trigger: 'a[name="o_wslides_list_slide_add_quizz"]', + extra_trigger: '.o_wslides_slides_list_slide:hover', + content: _t("If you want to be sure that attendees have understood and memorized the content, you can add a Quiz on the lesson. Click on <b>Add Quiz</b>."), +}, { + trigger: 'input[name="question-name"]', + content: _t("Enter your <b>Question</b>. Be clear and concise."), + position: 'left', + width: 330, +}, { + trigger: 'input.o_wslides_js_quiz_answer_value', + content: _t("Enter at least two possible <b>Answers</b>."), + position: 'left', + width: 290, +}, { + trigger: 'a.o_wslides_js_quiz_is_correct', + content: _t("Mark the correct answer by checking the <b>correct</b> mark."), + position: 'right', + width: 230, +}, { + trigger: 'i.o_wslides_js_quiz_comment_answer:last', + content: _t("You can add <b>comments</b> on answers. This will be visible with the results if the user select this answer."), + position: 'right', + +}, { + trigger: 'a.o_wslides_js_quiz_validate_question', + content: _t("<b>Save</b> your question."), + position: 'left', + width: 170, +}, { + trigger: 'li.breadcrumb-item:nth-child(2)', + content: _t("Click on your <b>Course</b> to go back to the table of content."), + position: 'top', +}, { + trigger: 'label.js_publish_btn', + content: _t("Once you're done, don't forget to <b>Publish</b> your course."), + position: 'bottom', +}, { + trigger: 'a.o_wslides_js_slides_list_slide_link', + content: _t("Congratulations, you've created your first course.<br/>Click on the title of this content to see it in fullscreen mode."), + position: 'bottom', +}]); + +}); diff --git a/addons/website_slides/static/src/js/website_slides.editor.js b/addons/website_slides/static/src/js/website_slides.editor.js new file mode 100644 index 00000000..8ee780a2 --- /dev/null +++ b/addons/website_slides/static/src/js/website_slides.editor.js @@ -0,0 +1,188 @@ +odoo.define('website_slides.editor', function (require) { +"use strict"; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var QWeb = core.qweb; +var WebsiteNewMenu = require('website.newMenu'); +var TagCourseDialog = require('website_slides.channel_tag.add').TagCourseDialog; +var wUtils = require('website.utils'); + +var _t = core._t; + + +var ChannelCreateDialog = Dialog.extend({ + template: 'website.slide.channel.create', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website_slides/static/src/xml/website_slides_channel.xml', + '/website_slides/static/src/xml/website_slides_channel_tag.xml'] + ), + events: _.extend({}, Dialog.prototype.events, { + 'change input#tag_ids' : '_onChangeTag', + }), + custom_events: _.extend({}, Dialog.prototype.custom_events, { + 'tag_refresh': '_onTagRefresh', + 'tag_remove_new': '_onTagRemoveNew', + }), + /** + * @override + * @param {Object} parent + * @param {Object} options + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t("New Course"), + size: 'medium', + buttons: [{ + text: _t("Create"), + classes: 'btn-primary', + click: this._onClickFormSubmit.bind(this) + }, { + text: _t("Discard"), + close: true + },] + }); + this._super(parent, options); + }, + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + var $input = self.$('#tag_ids'); + $input.select2({ + width: '100%', + allowClear: true, + formatNoMatches: false, + multiple: true, + selection_data: false, + formatSelection: function (data) { + if (data.tag) { + data.text = data.tag; + } + return data.text; + }, + createSearchChoice: function(term, data) { + var addedTags = $(this.opts.element).select2('data'); + if (_.filter(_.union(addedTags, data), function (tag) { + return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0; + }).length === 0) { + if (this.opts.can_create) { + return { + id: _.uniqueId('tag_'), + create: true, + tag: term, + text: _.str.sprintf(_t("Create new Tag '%s'"), term), + }; + } else { + return undefined; + } + } + }, + fill_data: function (query, data) { + var that = this, + tags = {results: []}; + _.each(data, function (obj) { + if (that.matcher(query.term, obj.name)) { + tags.results.push({id: obj.id, text: obj.name}); + } + }); + query.callback(tags); + }, + query: function (query) { + var that = this; + // fetch data only once and store it + if (!this.selection_data) { + self._rpc({ + route: '/slides/channel/tag/search_read', + params: { + fields: ['name'], + domain: [], + } + }).then(function (data) { + that.can_create = data.can_create; + that.fill_data(query, data.read_results); + that.selection_data = data.read_results; + }); + } else { + this.fill_data(query, this.selection_data); + } + } + }); + }); + }, + _onClickFormSubmit: function (ev) { + var $form = this.$("#slide_channel_add_form"); + var $title = this.$("#title"); + if (!$title[0].value){ + $title.addClass('border-danger'); + this.$("#title-required").removeClass('d-none'); + } else { + $form.submit(); + } + }, + _onChangeTag: function (ev) { + var self = this; + var tags = $(ev.currentTarget).select2('data'); + tags.forEach(function (element) { + if (element.create) { + new TagCourseDialog(self, { defaultTag: element.text }).open(); + } + }); + }, + /** + * Replace the new tag ID by its real ID + * @param ev + * @private + */ + _onTagRefresh: function (ev) { + var $tag_ids = $('#tag_ids'); + var tags = $tag_ids.select2('data'); + tags.forEach(function (element) { + if (element.create) { + element.id = ev.data.tag_id; + element.create = false; + } + }); + $tag_ids.select2('data', tags); + // Set selection_data to false to force tag reload + $tag_ids.data('select2').opts.selection_data = false; + }, + /** + * Remove the created tag if the user clicks on 'Discard' on the create tag Dialog + * @private + */ + _onTagRemoveNew: function () { + var tags = $('#tag_ids').select2('data'); + tags = tags.filter(function (value) { + return !value.create; + }); + $('#tag_ids').select2('data', tags); + }, +}); + +WebsiteNewMenu.include({ + actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, { + new_slide_channel: '_createNewSlideChannel', + }), + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Displays the popup to create a new slide channel, + * and redirects the user to this channel. + * + * @private + * @returns {Promise} Unresolved if there is a redirection + */ + _createNewSlideChannel: function () { + var self = this; + var def = new Promise(function (resolve) { + var dialog = new ChannelCreateDialog(self, {}); + dialog.open(); + dialog.on('closed', self, resolve); + }); + return def; + }, +}); +}); diff --git a/addons/website_slides/static/src/scss/rating_rating_views.scss b/addons/website_slides/static/src/scss/rating_rating_views.scss new file mode 100644 index 00000000..fa920c1b --- /dev/null +++ b/addons/website_slides/static/src/scss/rating_rating_views.scss @@ -0,0 +1,15 @@ +$o-kanban-large-record-width: 350px; + +.o_kanban_view.o_slide_rating_kanban { + .o_kanban_record { + width: $o-kanban-large-record-width + } + + .o_slide_rating_kanban_left { + min-width: 80px; + + .o_slide_rating_value { + font-size: 4rem; + } + } +} diff --git a/addons/website_slides/static/src/scss/slide_views.scss b/addons/website_slides/static/src/scss/slide_views.scss new file mode 100644 index 00000000..035f4699 --- /dev/null +++ b/addons/website_slides/static/src/scss/slide_views.scss @@ -0,0 +1,17 @@ +$o-kanban-large-record-width: 400px; + +.o_kanban_view.o_slide_kanban { + .o_kanban_group:not(.o_column_folded) { + width: $o-kanban-large-record-width + 2*$o-kanban-group-padding; + } + .o_kanban_record { + width: $o-kanban-large-record-width; + } +} + +table.o_section_list_view tr.o_data_row.o_is_section { + font-weight: bold; + background-color: #DDD; + border-top: 1px solid #BBB; + border-bottom: 1px solid #BBB; +} diff --git a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss new file mode 100644 index 00000000..c00bc120 --- /dev/null +++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss @@ -0,0 +1,124 @@ + +.o_wslides_fs_main { + @include o-position-absolute(0,0,0,0); + z-index: $zindex-website-header + 1; + background-image: linear-gradient(120deg, $o-wslides-color-dark2, $o-wslides-color-dark3); + + .o_wslides_slide_fs_header { + background-image: linear-gradient(-6deg, $o-wslides-color-dark1, $o-wslides-color-dark2); + height: 50px; + + > div > a { + background-color: rgba($o-wslides-color-dark3, 0.5); + @include o-hover-text-color(rgba(white, 0.8), white); + text-decoration: none!important; + + + a { + margin-left: 1px; + } + + &:hover { + background-color: rgba($o-wslides-color-dark3, 0.2); + } + + &.active{ + background-color: $o-wslides-color-dark1; + color: #fff; + } + } + } + + .o_wslides_fs_player, .o_wslides_fs_sidebar, .o_wslides_fs_sidebar_content { + transition: all .2s ease-in; + } + + .o_wslides_fs_sidebar { + background-image: linear-gradient(160deg, $o-wslides-color-dark1, $o-wslides-color-dark2); + position: relative; + z-index: $zindex-fixed; + + .o_wslides_fs_sidebar_content { + min-width: $o-wslides-fs-side-width; + } + + .o_wslides_fs_toggle_sidebar { + @include o-position-absolute(0, auto, 0, 100%); + width: 700px; + background: rgba(black, 0.2); + } + + @include media-breakpoint-down (md) { + @include o-position-absolute(0, auto, 0, 0); + box-shadow: 5px 0 15px rgba(black, 0.2); + + &.o_wslides_fs_sidebar_hidden { + display: none; + } + } + + @include media-breakpoint-up (md) { + width: $o-wslides-fs-side-width; + + &.o_wslides_fs_sidebar_hidden { + width: 0; + + .o_wslides_fs_sidebar_content { + transform: translateX(-100%); + } + } + } + + a { + text-decoration: none !important; + @include o-hover-text-color(rgba(white, 0.8), white); + } + + .o_wslides_fs_sidebar_section { + background-color: rgba($o-wslides-color-dark3, 0.3); + margin-bottom: 1px; + } + + .o_wslides_fs_sidebar_section_slides li { + color: rgba(white, 0.8); + line-height: 1.3; + + &.active { + box-shadow: inset 2px 0 0 theme-color('primary'); + background-color: rgba($o-wslides-color-dark3, 0.5); + + &, a { + color: white; + } + } + + .o_wslides_fs_slide_name { + line-height: 1; + } + } + } + + .o_wslides_js_lesson_quiz_question { + .list-group-item { + font-size: 1rem; + + input:checked + i.fa-circle { + color: $primary !important; + } + } + + &.disabled { + opacity: 0.5; + pointer-events: none; + } + + &.completed-disabled{ + pointer-events: none; + } + } +} + +.modal-open { + > .modal-backdrop { + z-index: $zindex-modal-backdrop; + } +} diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss new file mode 100644 index 00000000..c3b87821 --- /dev/null +++ b/addons/website_slides/static/src/scss/website_slides.scss @@ -0,0 +1,591 @@ +$MAX-Z-INDEX : 2147483647 !default; + +// Retrive the tab's height by summ its properties +$o-wslides-tabs-height: ($nav-link-padding-y*2) + ($font-size-base * $line-height-base); + +// Overal page bg-color: Blend it 'over' the color chosen by the user +// ($body-bg), rather than force it replacing the variable's value. +$o-wslides-color-bg: mix($body-bg, #efeff4); + +$o-wslides-color-dark1: #47525f; +$o-wslides-color-dark2: #1f262d; +$o-wslides-color-dark3: #101216; +$o-wslides-fs-side-width: 300px; + + +// Common to new slides pages +// ************************************************** +.o_wslides_gradient { + background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%)); +} + +.o_wslides_course_pict { + @include size(100%); + object-fit: cover; + + @include media-breakpoint-up(md) { + border: 1px solid darken(#875A7B, 10%); + border-bottom-width: 0; + } +} + +.o_wslides_arrow { + position: absolute; + height: 24px; + margin-left: -5px; + margin-top: 10px; + line-height: 1.8em; + padding-left: 8px; + padding-right: 5px; + background: #17A2B8; + color: white; + box-shadow: 0px 0px 3px gray; + z-index: 1; + + text-decoration: none; + + &:after { + // the triangle + content: ""; + position: absolute; + height: 0px; + width: 0px; + right: 0; + margin-right: -15px; + border-left: 15px solid #17A2B8; + border-bottom: 12px solid transparent; + border-top: 12px solid transparent; + } +} + +// Color tags according to assigned background color. +.o_wslides_channel_tag { + vertical-align: middle; + @for $size from 1 through length($o-colors) { + &.o_tag_color_#{$size - 1} { + $background-color: white; + // no color selected + @if $size == 1 { + & { + color: black; + background-color: $background-color; + box-shadow: inset 0 0 0 1px nth($o-colors, $size); + } + } @else { + $background-color: nth($o-colors, $size); + & { + color: white; + background-color: $background-color; + } + } + @at-root a#{&} { + &:hover { + color: color-yiq($background-color); + background-color: darken($background-color, 10%); + } + } + } + } +} + +.o_wslides_body { + background-color: $o-wslides-color-bg; + + .o_wslides_home_nav { + top: -40px; + + @include media-breakpoint-up(lg) { + font-size: 1rem; + + .o_wslides_nav_navbar_right { + padding-left: $spacer; + margin-left: auto; + border-left: 1px solid $border-color; + } + } + } + + .o_wslides_js_slide_like_up, + .o_wslides_js_slide_like_down { + &:not(.disabled) { + cursor: pointer; + color: $link-color; + } + } + + .o_wslides_js_lesson_quiz { + i.o_wslides_js_quiz_icon { + cursor: pointer; + } + + i.o_wslides_js_quiz_icon:hover { + color: black !important; + } + } + + .o_wslides_js_lesson_quiz_question { + .list-group-item { + font-size: 1rem; + + input:checked + i.fa-circle { + color: $primary !important; + } + } + + &.disabled { + opacity: 0.5; + pointer-events: none; + } + + &.completed-disabled{ + pointer-events: none; + } + } + + a.o_wslides_js_quiz_is_correct { + color: black; + input:checked + i.fa-check-circle-o { + color: $primary !important; + } + } + + .o_wslides_js_quiz_sequence_highlight { + background-color: #1252F3; + height: 1px; + z-index: 3; + + &:before, &:after { + content: ""; + @include size(6px); + display: block; + border-radius: 100%; + background-color: inherit; + @include o-position-absolute(-2px, -2px); + } + + &:after { + right: auto; + left: -2px; + } + } + + // tools + // **************************************** + .text_small_caps { + font-variant: small-caps; + } + + .o_wslides_entry_muted { + opacity: 0.5; + } + + // Solve an overfow issue caused in some + // circumstances by flex containers. + hr { + min-height: 1px; + } + + // Truncate text descriptions to a specific number of lines. + // If '-webkit-line-clamp' is not supported, a less effective + // 'line-height' fallback will be used instead. + $truncate-limits: 2, 3, 10; + + @each $limit in $truncate-limits { + .o_wslides_desc_truncate_#{$limit} { + $line-height: 1.3; + max-height: $limit * $line-height * 1.2em; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + line-height: $line-height; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: $limit; + } + } +} + +// New home page +// ************************************************** +.o_wslides_home_main { + .o_wslides_home_aside_loggedin { + @include media-breakpoint-up(lg) { + background: none; + border: none; + } + } + + .o_wprofile_progress_circle { + margin-left: auto; + margin-right: auto; + max-width: 200px; + } +} + +// Courses Card +// ************************************************** +.o_wslides_course_card.o_wslides_course_unpublished { + opacity: 0.5; +} + +// New course page +// ************************************************** + +.o_wslides_course_sidebar { + border: 1px solid $border-color; + + @include media-breakpoint-up(md) { + border-top-width: 0; + } + + .o_wslides_js_channel_unsubscribe { + > .fa-times { + display: none; + } + + &:hover { + > .fa-check { + display: none; + } + + > .fa-times { + display: inline-block; + } + } + } + + .o_wslides_js_channel_enroll { + cursor: pointer; + + &:hover, &:hover .o_wslides_enroll_msg small { + font-weight: bold; + } + } + + .o_wslides_enroll_msg { + p { + display: inline-block; + margin-bottom: 0px; + } + } +} + +@mixin o-wslides-tabs($tab-active-color: $o-wslides-color-bg) { + margin-top: ($o-wslides-tabs-height * -1); + border-bottom: 0; + + .nav-link { + border-radius: 0; + border-width: 0 1px; + line-height: $line-height-base; + @include o-hover-text-color(rgba(white, 0.8), white); + + & { + border-color: transparent; + } + + &:hover { + background: #3d2938; + } + + &.active { + color: color-yiq($tab-active-color); + background: $tab-active-color; + border-color: $tab-active-color; + } + } +} + +@mixin o-wslides-header-bar() { + &:before { + content: ""; + @include o-position-absolute(auto, 0, 0, 0); + height: $o-wslides-tabs-height; + background: rgba(black, 0.2); + } +} + +.o_wslides_course_nav { + @include o-position-absolute(0,0,auto,0); + border-width: 1px 0; + + &, .o_wslides_course_nav_search { + background-color: rgba(white, 0.05); + border-color: rgba(white, 0.1); + border-style: solid; + } + + .o_wslides_course_nav_search { + border-width: 0 1px; + } + + .breadcrumb-item.active a, .breadcrumb-item a:hover { + color: white; + } + + .breadcrumb-item a, .breadcrumb-item + .breadcrumb-item::before, .o_wslides_course_nav_search input::placeholder { + color: rgba(white, 0.8); + } +} + + +.o_wslides_course_header { + @include media-breakpoint-up(md) { + @include o-wslides-header-bar(); + } +} + +.o_wslides_course_doc_header { + @include o-wslides-header-bar(); +} + +.o_wslides_course_main { + .o_wslides_nav_tabs { + @include media-breakpoint-up(md) { + @include o-wslides-tabs(); + } + + @include media-breakpoint-only(xs) { + overflow-x: auto; + overflow-y: hidden; + line-height: 1.51; + + li { + white-space: nowrap; + } + } + } + + .o_wslides_doc_nav_tabs { + @include o-wslides-tabs($gray-100); + } + + .o_wslides_tabs_content { + @include media-breakpoint-down(sm) { + background-color: $nav-tabs-link-active-bg; + padding:0 ($grid-gutter-width * 0.5); + } + + @include media-breakpoint-only(xs) { + margin: 0 ($grid-gutter-width * -0.5); + } + } + + // Slides list reordering widget + .o_wslides_slides_list { + .o_wslides_slide_list_category_header { + z-index: 1; + + & + ul { + z-index: 0; + } + } + + .o_text_link { + text-decoration: none!important; + + > * { + text-decoration: none!important; + color: map-get($grays, "600"); + } + + &:hover > * { + color: inherit; + } + } + + .o_wslides_slides_list_drag { + cursor: pointer; + + i { opacity: 0.4; } + &:hover i { opacity: 1; } + } + + .o_wslides_slide_list_category_header, .o_wslides_slides_list_slide { + border: 1px solid $border-color; + } + + .o_wslides_slides_list_slide { + + a { + text-decoration: none; + } + + .badge-hide { + display: none; + } + + &:hover .badge-hide { + display: block; + } + } + + .o_wslides_slides_list_slide_hilight { + background-color: #1252F3; + height: 1px; + z-index: 3; + + &:before, &:after { + content: ""; + @include size(6px); + display: block; + border-radius: 100%; + background-color: inherit; + @include o-position-absolute(-2px, -2px); + } + + &:after { + right: auto; + left: -2px; + } + } + } +} + + +// New lesson page (not fullscreens) +// ************************************************** +.o_wslides_lesson_main { + .o_wslides_lesson_aside { + .o_wslides_lesson_aside_collapse.collapsed { + transform: rotate(90deg); + } + + .o_wslides_lesson_aside_list { + @include media-breakpoint-up (lg) { + top: -58px; + } + } + + .o_wslides_lesson_aside_list { + .o_wslides_lesson_aside_list_link { + @include o-hover-text-color($gray-600, $headings-color ); + + .o_wslides_lesson_link_name { + line-height: 1.2; + } + + &.active { + box-shadow:inset 2px 0 theme-color('primary'); + } + + &:hover .o_wslides_lesson_link_name { + color: $headings-color; + } + } + } + } + + .o_wslides_lesson_content { + .o_wslides_lesson_nav { + .nav-link { + background-color: transparent; + border: 0; + border-bottom: 1px solid $border-color; + color: $gray-600; + + &.active { + border-bottom: 1px solid $success; + color: $gray-800; + } + } + } + } +} + + +// Modals +// ************************************************** + +.o_wslides_quiz_modal { + @include media-breakpoint-up (sm) { + .modal-body { + overflow: visible!important; + + .o_wslides_quiz_modal_close_btn { + right: 5px; + } + + .o_wslides_gradient { + width: 42%; + } + } + + .modal-content { + height: 461px; + + .o_wslides_quiz_modal_hero { + margin-left: -30px; + position: absolute; + margin-top: -45px; + } + } + } + + .progress { + border-radius:0; + overflow:visible; + height: 8px; + + .progress-bar { + transition: width 0.8s ease; + + &.no-transition { + transition: none; + } + } + } + + .tooltip > .tooltip-inner { + background-color: #875A7B; + padding:5px 15px; + font-weight:bold; + font-size:13px; + } + + .tooltip > .arrow { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #875A7B; + } + + .tooltip > .arrow::before { + display: none; + } +} + +#slide_channel_add_form input[name="channel_type"]:checked + label { + border: 4px solid $primary; + box-shadow: 3px 3px 5px gray; +} + +// Embed PDFViewer +// ************************************************** +#PDFViewer.o_wslides_fs_pdf_viewer { + background-image: linear-gradient(120deg, $o-wslides-color-dark2, $o-wslides-color-dark3); + + #PDFViewerNav { + background-image: linear-gradient(120deg, $o-wslides-color-dark1, $o-wslides-color-dark2); + } + + .oe_slides_panel_footer a, .oe_slides_share_bar a { + @include o-hover-text-color(rgba(white, 0.7), white); + + &.disabled { + @include o-hover-text-color(rgba(white, 0.2), rgba(white, 0.2)); + cursor: default; + } + } + + .oe_slide_embed_option { + @include o-position-absolute(0,0,0,0); + } +} + +.oe_slides_share_bar{ + padding: 10px 0; +} + +.oe_show_footer { + z-index: $MAX-Z-INDEX; // Looks terrible but seems necessary due to fullscreen & canvas in PDFSlidesViewer +} diff --git a/addons/website_slides/static/src/scss/website_slides_profile.scss b/addons/website_slides/static/src/scss/website_slides_profile.scss new file mode 100644 index 00000000..35f85b4c --- /dev/null +++ b/addons/website_slides/static/src/scss/website_slides_profile.scss @@ -0,0 +1,4 @@ +// Quest - Course Card +.o_wprofile_slides_course_card_body { + cursor: pointer; +} diff --git a/addons/website_slides/static/src/tests/tours/slides_course_member.js b/addons/website_slides/static/src/tests/tours/slides_course_member.js new file mode 100644 index 00000000..7f30b945 --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slides_course_member.js @@ -0,0 +1,141 @@ +odoo.define('website_slides.tour.slide.course.member', function (require) { +'use strict'; + +var tour = require('web_tour.tour'); + +/** + * Global use case: + * an user (either employee, website publisher or portal) joins a public + course; + * he has access to the full course content when he's a member of the + course; + * he uses fullscreen player to complete the course; + * he rates the course; + */ +tour.register('course_member', { + url: '/slides', + test: true +}, [ +// eLearning: go on free course and join it +{ + trigger: 'a:contains("Basics of Gardening - Test")' +}, { + trigger: 'a:contains("Join Course")' +}, { + trigger: '.o_wslides_js_course_join:contains("You\'re enrolled")', + run: function () {} // check membership +}, { + trigger: 'a:contains("Gardening: The Know-How")', +}, +// eLearning: follow course by cliking on first lesson and going to fullscreen player +{ + trigger: '.o_wslides_fs_sidebar_list_item div:contains("Home Gardening")' +}, { + trigger: '.o_wslides_fs_sidebar_header', + run: function () { + // check navigation with arrow keys + var event = jQuery.Event("keydown"); + event.key = "ArrowLeft"; + // go back once + $(document).trigger(event); + // check that it selected the previous tab + if ($('.o_wslides_fs_sidebar_list_item.active:contains("Gardening: The Know-How")').length === 0) { + return; + } + // getting here means that navigation worked + $('.o_wslides_fs_sidebar_header').addClass('navigation-success-1'); + } +}, { + trigger: '.o_wslides_fs_sidebar_header.navigation-success-1', + extra_trigger: '.o_wslides_progress_percentage:contains("40")', + run: function () { + // check navigation with arrow keys + var event = jQuery.Event("keydown"); + event.key = "ArrowRight"; + $(document).trigger(event); + // check that it selected the next/next tab + if ($('.o_wslides_fs_sidebar_list_item.active:contains("Home Gardening")').length === 0) { + return; + } + // getting here means that navigation worked + $('.o_wslides_fs_sidebar_header').addClass('navigation-success-2'); + } +}, { + trigger: '.o_wslides_progress_percentage:contains("40")', + run: function () {} // check progression +}, { + trigger: '.o_wslides_fs_sidebar_header.navigation-success-2', + extra_trigger: '.o_wslides_progress_percentage:contains("40")', + run: function () { + // check navigation with arrow keys + var event = jQuery.Event("keydown"); + event.key = "ArrowRight"; + setTimeout(function () { + $(document).trigger(event); + // check that it selected the next/next tab + if ($('.o_wslides_fs_sidebar_list_item.active:contains("Mighty Carrots")').length === 0) { + return; + } + // getting here means that navigation worked + $('.o_wslides_fs_sidebar_header').addClass('navigation-success-3'); + }, 300); + } +}, { + trigger: '.o_wslides_progress_percentage:contains("60")', + run: function () {} // check progression +}, { + trigger: '.o_wslides_fs_sidebar_header.navigation-success-3', + extra_trigger: '.o_wslides_progress_percentage:contains("60")', + run: function () {} // check that previous step succeeded +}, { + trigger: '.o_wslides_fs_sidebar_list_item div:contains("How to Grow and Harvest The Best Strawberries | Basics")' +}, { + trigger: '.o_wslides_fs_sidebar_section_slides li:contains("How to Grow and Harvest The Best Strawberries | Basics") .o_wslides_slide_completed', + run: function () {} // check that video slide is marked as 'done' +}, { + trigger: '.o_wslides_progress_percentage:contains("80")', + run: function () {} // check progression +}, +// eLearning: last slide is a quiz, complete it +{ + trigger: '.o_wslides_fs_sidebar_list_item div:contains("Test your knowledge")' +}, { + trigger: '.o_wslides_js_lesson_quiz_question:first .list-group a:first' +}, { + trigger: '.o_wslides_js_lesson_quiz_question:last .list-group a:first' +}, { + trigger: '.o_wslides_js_lesson_quiz_submit' +}, { + trigger: 'a:contains("End course")' +}, +// eLearning: ending course redirect to /slides, course is completed now +{ + trigger: 'div:contains("Basics of Gardening") span:contains("Completed")', + run: function () {} // check that the course is marked as completed +}, +// eLearning: go back on course and rate it (new rate or update it, both should work) +{ + trigger: 'a:contains("Basics of Gardening")' +}, { + trigger: 'button[data-target="#ratingpopupcomposer"]' +}, { + trigger: 'form.o_portal_chatter_composer_form i.fa:eq(4)', + extra_trigger: 'div.modal_shown', + run: 'click', + in_modal: false, +}, { + trigger: 'form.o_portal_chatter_composer_form textarea', + run: 'text This is a great course. Top !', + in_modal: false, +}, { + trigger: 'button.o_portal_chatter_composer_btn', + in_modal: false, +}, { + trigger: 'a[id="review-tab"]' +}, { + trigger: '.o_portal_chatter_message:contains("This is a great course. Top !")', + run: function () {}, // check review is correctly added +} +]); + +}); diff --git a/addons/website_slides/static/src/tests/tours/slides_course_member_yt.js b/addons/website_slides/static/src/tests/tours/slides_course_member_yt.js new file mode 100644 index 00000000..b060384d --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slides_course_member_yt.js @@ -0,0 +1,67 @@ +odoo.define('website_slides.tour.slide.course.member.youtube', function (require) { +'use strict'; + +var tour = require('web_tour.tour'); +var FullScreen = require('website_slides.fullscreen'); + +/** + * Alter this method for test purposes. + * This will make the video start at 10 minutes. + * As it lasts 10min24s, it will mark it as completed immediately. + */ +FullScreen.include({ + _renderSlide: function () { + + var slide = this.get('slide'); + slide.embedUrl += '&start=260'; + this.set('slide', slide); + + return this._super.call(this, arguments); + } +}); + +/** + * Global use case: + * an user (either employee, website publisher or portal) joins a public + course; + * he has access to the full course content when he's a member of the + course; + * he uses fullscreen player to complete the course; + * he rates the course; + */ +tour.register('course_member_youtube', { + url: '/slides', + test: true +}, [ +// eLearning: go on /all, find free course and join it +{ + trigger: 'a.o_wslides_home_all_slides' +}, { + trigger: 'a:contains("Choose your wood")' +}, { + trigger: 'a:contains("Join Course")' +}, { + trigger: '.o_wslides_js_course_join:contains("You\'re enrolled")', + run: function () {} // check membership +}, { + trigger: 'a:contains("Comparing Hardness of Wood Species")', +}, { + trigger: '.o_wslides_progress_percentage:contains("50")', + run: function () {} // check progression +}, { + trigger: 'a:contains("Wood Bending With Steam Box")', +}, { + trigger: '.player', + run: function () {} // check player loading +}, { + trigger: '.o_wslides_fs_sidebar_section_slides li:contains("Wood Bending With Steam Box") .o_wslides_slide_completed', + run: function () {} // check that video slide is marked as 'done' +}, { + trigger: '.o_wslides_progress_percentage:contains("100")', + run: function () {} // check progression +}, { + trigger: 'a:contains("Back to course")' +} +]); + +}); diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js new file mode 100644 index 00000000..98cca857 --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js @@ -0,0 +1,96 @@ +odoo.define('website_slides.tour.slide.course.publisher', function (require) { +'use strict'; + +var tour = require('web_tour.tour'); +var slidesTourTools = require('website_slides.tour.tools'); + +/** + * Global use case: + * a user (website publisher) creates a course; + * he updates it; + * he creates some lessons in it; + * he publishes it; + */ +tour.register('course_publisher', { + url: '/slides', + test: true +}, [{ + content: 'eLearning: click on New (top-menu)', + trigger: 'li.o_new_content_menu a' +}, { + content: 'eLearning: click on New Course', + trigger: 'a:contains("Course")' +}, { + content: 'eLearning: set name', + trigger: 'input[name="name"]', + run: 'text How to Déboulonnate', +}, { + content: 'eLearning: click on tags', + trigger: 'ul.select2-choices:first', +}, { + content: 'eLearning: select gardener tag', + trigger: 'div.select2-result-label:contains("Gardener")', + in_modal: false, +}, { + content: 'eLearning: set description', + trigger: 'input[name="description"]', + run: 'text Déboulonnate is very common at Fleurus', +}, { + content: 'eLearning: we want reviews', + trigger: 'input[name="allow_comment"]', +}, { + content: 'eLearning: seems cool, create it', + trigger: 'button:contains("Create")', +}, { + content: 'eLearning: launch course edition', + trigger: 'li[id="edit-page-menu"] a', +}, { + content: 'eLearning: double click image to edit it', + trigger: 'img.o_wslides_course_pict', + run: 'dblclick', +}, { + content: 'eLearning: click pâtissière', + trigger: 'img[title="s_company_team_image_4.png"]', +}, { + content: 'eLearning: is the pâtissière set ?', + trigger: 'img.o_wslides_course_pict', + run: function () { + if ($('img.o_wslides_course_pict').attr('src').endsWith('s_team_member_4.png')) { + $('img.o_wslides_course_pict').addClass('o_wslides_tour_success'); + } + }, +}, { + content: 'eLearning: the pâtissière is set !', + trigger: 'img.o_wslides_course_pict.o_wslides_tour_success', +}, { + content: 'eLearning: save course edition', + trigger: 'button[data-action="save"]', +}, { + content: 'eLearning: course create with current member', + extra_trigger: 'body:not(.editor_enable)', // wait for editor to close + trigger: '.o_wslides_js_course_join:contains("You\'re enrolled")', + run: function () {} // check membership +} +].concat( + slidesTourTools.addExistingCourseTag(), + slidesTourTools.addNewCourseTag('The Most Awesome Course'), + slidesTourTools.addSection('Introduction'), + slidesTourTools.addVideoToSection('Introduction'), + [{ + content: 'eLearning: publish newly added course', + trigger: 'span:contains("Dschinghis Khan - Moskau 1979")', // wait for slide to appear + // trigger: 'span.o_wslides_js_slide_toggle_is_preview:first', + run: function () { + $('span.o_wslides_js_slide_toggle_is_preview:first').click(); + } +}] +// [ +// { +// content: 'eLearning: move new course inside introduction', +// trigger: 'div.o_wslides_slides_list_drag', +// // run: 'drag_and_drop div.o_wslides_slides_list_drag ul.ui-sortable:first', +// run: 'drag_and_drop div.o_wslides_slides_list_drag a.o_wslides_js_slide_section_add', +// }] +)); + +}); diff --git a/addons/website_slides/static/src/tests/tours/slides_full_screen_web_editor.js b/addons/website_slides/static/src/tests/tours/slides_full_screen_web_editor.js new file mode 100644 index 00000000..26aeafe0 --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slides_full_screen_web_editor.js @@ -0,0 +1,39 @@ +odoo.define('website_slides.tour.fullscreen.edition.publisher', function (require) { +'use strict'; + +var tour = require('web_tour.tour'); + +/** + * Global use case: + * - a user (website publisher) lands on the fullscreen view of a course ; + * - he clicks on the website editor "Edit" button ; + * - he is redirected to the non-fullscreen view with the editor opened. + * + * This tour tests a fix made when editing a course in fullscreen view. + * See "Fullscreen#_onWebEditorClick" for more information. + * + */ +tour.register('full_screen_web_editor', { + url: '/slides', + test: true +}, [{ + // open to the course + trigger: 'a:contains("Basics of Gardening")' +}, { + // click on a slide to open the fullscreen view + trigger: 'a.o_wslides_js_slides_list_slide_link:contains("Home Gardening")' +}, { + trigger: '.o_wslides_fs_main', + run: function () {} // check we land on the fullscreen view +}, { + // click on the main "Edit" button to open the web editor + trigger: '#edit-page-menu a[data-action="edit"]', +}, { + trigger: '.o_wslides_lesson_main', + run: function () {} // check we are redirected on the detailed view +}, { + trigger: 'body.editor_enable', + run: function () {} // check the editor is automatically opened on the detailed view +}]); + +}); diff --git a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js new file mode 100644 index 00000000..5b5f525c --- /dev/null +++ b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js @@ -0,0 +1,141 @@ +odoo.define('website_slides.tour.tools', function (require) { +'use strict'; + +/* + * PUBLISHER / CONTENT CREATION + */ + +var addSection = function (sectionName) { + return [ +{ + content: 'eLearning: click on Add Section', + trigger: 'a.o_wslides_js_slide_section_add', +}, { + content: 'eLearning: set section name', + trigger: 'input[name="name"]', + run: 'text ' + sectionName, +}, { + content: 'eLearning: create section', + trigger: 'footer.modal-footer button:contains("Save")' +}, { + content: 'eLearning: section created empty', + trigger: 'div.o_wslides_slide_list_category_header:contains("' + sectionName + '")', +}]; +}; + +var addVideoToSection = function (sectionName, saveAsDraft) { + var base_steps = [ +{ + content: 'eLearning: add content to section', + trigger: 'div.o_wslides_slide_list_category_header:contains("' + sectionName + '") a:contains("Add Content")', +}, { + content: 'eLearning: click on video', + trigger: 'a[data-slide-type=video]', +}, { + content: 'eLearning: fill video link', + trigger: 'input[name=url]', + run: 'text https://www.youtube.com/watch?v=NvS351QKFV4&list=PLtVFNIekBzqIfO4u4n78i43etfw2n1St8&index=2&t=0s', +}, { + content: 'eLearning: click outside to trigger onchange', + trigger: 'div.o_w_slide_upload_modal_container', + run: 'click', +}]; + if (saveAsDraft) { + base_steps = [].concat(base_steps, [{ + content: 'eLearning: save as draft slide', + extra_trigger: 'div.o_slide_preview img:not([src="/website_slides/static/src/img/document.png"])', // wait for onchange to perform its duty + trigger: 'footer.modal-footer button:contains("Save as Draft")', +}]); + } + else { + base_steps = [].concat(base_steps, [{ + content: 'eLearning: create and publish slide', + extra_trigger: 'div.o_slide_preview img:not([src="/website_slides/static/src/img/document.png"])', // wait for onchange to perform its duty + trigger: 'footer.modal-footer button:contains("Publish")', +}]); + } + return base_steps; +}; + +var addWebPageToSection = function (sectionName, pageName) { + return [ +{ + content: 'eLearning: add content to section', + trigger: 'div.o_wslides_slide_list_category_header:contains("' + sectionName + '") a:contains("Add Content")', +}, { + content: 'eLearning: click on webpage', + trigger: 'a[data-slide-type=webpage]', +}, { + content: 'eLearning: fill webpage title', + trigger: 'input[name=name]', + run: 'text ' + pageName, +}, { + content: 'eLearning: click on tags', + trigger: 'ul.select2-choices:first', +}, { + content: 'eLearning: select Theory tag', + trigger: 'div.select2-result-label:contains("Theory")', + in_modal: false, +}, { + content: 'eLearning: fill webpage completion time', + trigger: 'input[name=duration]', + run: 'text 4', +}]; +}; + +var addExistingCourseTag = function () { + return [ +{ + content: 'eLearning: click on Add Tag', + trigger: 'a.o_wslides_js_channel_tag_add', +}, { + content: 'eLearning: click on tag dropdown', + trigger: 'a.select2-choice:first', +}, { + content: 'eLearning: select advanced tag', + trigger: 'div.select2-result-label:contains("Advanced")', + in_modal: false, +}, { + content: 'eLearning: add existing course tag', + trigger: 'footer.modal-footer button:contains("Add")' +}]; +}; + +var addNewCourseTag = function (courseTagName) { + return [ +{ + content: 'eLearning: click on Add Tag', + trigger: 'a.o_wslides_js_channel_tag_add', +}, { + content: 'eLearning: click on tag dropdown', + trigger: 'a.select2-choice:first', +}, { + content: 'eLearning: add a new course tag', + trigger: 'a.select2-choice:first', + run: function () { + // directly add new tag since we can assume select2 works correctly + $('#tag_id').select2('data', {id:'123', text: courseTagName, create: true}); + $('#tag_id').trigger('change'); + } +}, { + content: 'eLearning: click on tag group dropdown', + trigger: 'a.select2-choice:last', +}, { + content: 'eLearning: select Tags tag group', + trigger: 'div.select2-result-label:contains("Tags")', + in_modal: false, +}, { + content: 'eLearning: add new course tag', + trigger: 'footer.modal-footer button:contains("Add")' +}]; +}; + +return { + addSection: addSection, + addVideoToSection: addVideoToSection, + addWebPageToSection: addWebPageToSection, + addExistingCourseTag: addExistingCourseTag, + addNewCourseTag: addNewCourseTag, +}; + +}); diff --git a/addons/website_slides/static/src/xml/activity.xml b/addons/website_slides/static/src/xml/activity.xml new file mode 100644 index 00000000..a2f5b2c0 --- /dev/null +++ b/addons/website_slides/static/src/xml/activity.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates xml:space="preserve"> + <t t-extend="mail.activity_items"> + <t t-jquery=".o_thread_message .o_thread_message_core .o_thread_message_tools" t-operation="replace"> + <t t-if="activity.activity_type_id[1] == 'Access Request'"> + <t t-if="activity.user_id[0] === uid"> + <a role="button" class="btn btn-link btn-success text-muted text-success o_activity_link o_activity_action_grant_access" t-att-data-partner-id="activity.request_partner_id[0]"> + <i class="fa fa-check"/> Grant Access + </a> + <a role="button" class="btn btn-link btn-danger text-muted text-danger o_activity_link o_activity_action_refuse_access" t-att-data-partner-id="activity.request_partner_id[0]"> + <i class="fa fa-times"/> Refuse Access + </a> + </t> + </t> + <t t-else=""> + <div class="o_thread_message_tools btn-group"> + <t t-call="mail.activity_thread_message_tools"/> + </div> + </t> + </t> + </t> + + <t t-inherit="mail.Activity" t-inherit-mode="extension"> + <xpath expr="//*[@name='tools']" position="replace"> + <t t-if="activity.requestingPartner and activity.thread.model === 'slide.channel'"> + <div class="o_Activity_tools"> + <button class="o_Activity_toolButton o_Activity_grantAccessButton btn btn-link" t-on-click="_onGrantAccess"> + <i class="fa fa-check"/> Grant Access + </button> + <button class="o_Activity_toolButton o_Activity_refuseAccessButton btn btn-link" t-on-click="_onRefuseAccess"> + <i class="fa fa-times"/> Refuse Access + </button> + </div> + </t> + <t t-else="">$0</t> + </xpath> + </t> +</templates> diff --git a/addons/website_slides/static/src/xml/slide_course_join.xml b/addons/website_slides/static/src/xml/slide_course_join.xml new file mode 100644 index 00000000..0241915c --- /dev/null +++ b/addons/website_slides/static/src/xml/slide_course_join.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates> + <t t-name="slide.course.join"> + <div> + <a role="button" + class="btn btn-primary o_wslides_js_course_join_link text-uppercase font-weight-bold" + title="Join the Course" aria-label="Join the Course" + href="#"> + <t t-if="widget.channel.channelEnroll == 'public'"> + <t t-if="widget.publicUser"> + Sign in + </t> + <t t-else="" t-esc="widget.joinMessage" /> + </t> + </a> + </div> + </t> + + <t t-name="slide.course.join.request"> + <div> + <p>Do you want to request access to this course ?</p> + </div> + </t> +</templates> diff --git a/addons/website_slides/static/src/xml/slide_management.xml b/addons/website_slides/static/src/xml/slide_management.xml new file mode 100644 index 00000000..53397b20 --- /dev/null +++ b/addons/website_slides/static/src/xml/slide_management.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="slides.slide.archive"> + <div> + <p>Are you sure you want to archive this slide ?</p> + </div> + </t> + + <t t-name="slides.category.add"> + <div> + <form action="/slides/category/add" method="POST" id="slide_category_add_form"> + <input type="hidden" name="csrf_token" t-att-value="csrf_token"/> + <input type="hidden" name="channel_id" t-att-value="widget.channelId"/> + <div class="form-group row"> + <label for="section_name" class="col-sm-3 col-form-label">Section name</label> + <div class="col-sm-9"> + <input type="text" class="form-control" name="name" id="section_name" required="required"/> + </div> + </div> + </form> + </div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml new file mode 100644 index 00000000..aa7bcc3f --- /dev/null +++ b/addons/website_slides/static/src/xml/slide_quiz.xml @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-name="slide.slide.quiz"> + <div class="o_wslides_fs_quiz_container o_wslides_wrap h-100 w-100 overflow-auto pb-5"> + <div class="container"> + + <div t-foreach="widget.quiz.questions" t-as="question" + t-attf-class="o_wslides_js_lesson_quiz_question mt-3 mb-4 #{widget.slide.completed ? 'completed-disabled' : ''}" + t-att-data-question-id="question.id" t-att-data-title="question.question"> + <div class="h4"> + <small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/> + </div> + <div class="list-group"> + <t t-foreach="question.answer_ids" t-as="answer"> + <a t-att-data-answer-id="answer.id" href="#" + t-att-data-text="answer.text_value" + t-attf-class="o_wslides_quiz_answer list-group-item d-flex align-items-center list-group-item-action #{widget.slide.completed && answer.is_correct ? 'list-group-item-success' : '' }"> + + <label class="my-0 d-flex align-items-center justify-content-center mr-2"> + <input type="radio" + t-att-name="question.id" + t-att-value="answer.id" + class="d-none"/> + <i t-att-class="'fa fa-circle text-400' + (!(widget.slide.completed && answer.is_correct) ? '' : ' d-none')"></i> + <i class="fa fa-times-circle text-danger d-none"></i> + <i t-att-class="'fa fa-check-circle text-success' + (widget.slide.completed && answer.is_correct ? '' : ' d-none')"></i> + </label> + <span t-esc="answer.text_value"/> + </a> + </t> + <div class="o_wslides_quiz_answer_info list-group-item list-group-item-info d-none"> + <i class="fa fa-info-circle"/> + <span class="o_wslides_quiz_answer_comment"/> + </div> + </div> + </div> + <div t-if="!widget.slide.completed" class="o_wslides_js_lesson_quiz_validation border-top pt-3"/> + <div t-else="" class="row"> + <div class="o_wslides_js_lesson_quiz_validation col py-2 bg-100 mb-2 border-bottom"/> + </div> + </div> + </div> + </t> + + <t t-name="slide.slide.quiz.validation"> + <div id="validation"> + <div t-if="!widget.isMember"> + <div class="o_wslides_join_course alert alert-info d-flex align-items-center justify-content-between"> + <div t-if="widget.channel.channelEnroll == 'invite'"> + <b>This course is private. + <span t-if="widget.publicUser"> + Please + <a t-att-href="'/web/login?redirect=' + widget.redirectURL" class="font-weight-bold"> + sign in + </a> + to enroll. + </span> + <a t-else="" href="#" class="font-weight-bold o_wslides_js_channel_enroll" + t-att-data-channel-id="widget.channel.channelId"> + <span t-if="widget.channel.channelRequestedAccess" class="text-success"> + Responsible already contacted. + </span> + <span t-else=""> + Contact the responsible to enroll. + </span> + </a> + </b> + <span class="my-0 h4"> + <span title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2 py-1"> + + <t t-esc="widget.quiz.quizKarmaGain"/> XP + </span> + </span> + </div> + <div t-else="" class="w-100"> + <b class="h5 mb-0 o_wslides_quiz_join_course_message"> + <span t-if="widget.channel.channelEnroll == 'public'"> + <t t-if="widget.publicUser"> + Sign in and join the course to verify your answers! + </t> + <t t-else=""> + Join the course to take the quiz and verify your answers! + </t> + </span> + </b> + <span class="my-0 h4"> + <span title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2 py-1"> + + <t t-esc="widget.quiz.quizKarmaGain"/> XP + </span> + </span> + <div class="o_wslides_join_course_widget float-right"/> + </div> + </div> + <span t-if="widget.publicUser && widget.channel.signupAllowed" class="d-block mt-2"> + <span>Don't have an account ?</span> + <a class="font-weight-bold" t-att-href="'/web/signup?redirect=' + widget.url">Sign Up !</a> + </span> + </div> + <div t-else="" class="d-md-flex align-items-center justify-content-between"> + <div t-att-class="'d-flex align-items-center' + (widget.slide.completed ? ' alert alert-success my-0 py-1 px-3' : '')"> + <button t-if="! widget.slide.completed" role="button" title="Check answers" aria-label="Check answers" + class="btn btn-primary text-uppercase font-weight-bold o_wslides_js_lesson_quiz_submit">Check your answers</button> + <b t-else="" class="my-0 h5">Done !</b> + <span class="my-0 h5" style="line-height: 1"> + <span role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2"> + + <t t-if="!widget.slide.completed" t-esc="widget.quiz.quizKarmaGain"/><t t-else="" t-esc="widget.quiz.quizKarmaWon"/> XP + </span> + </span> + </div> + <div class="ml-auto mt-3 mt-md-0"> + <button t-if="widget.quiz.quizAttemptsCount > 0 && widget.slide.channelCanUpload" class="btn btn-light border o_wslides_js_lesson_quiz_reset"> + Reset + </button> + <button t-if="widget.slide.completed && widget.slide.hasNext" class="btn btn-primary o_wslides_quiz_continue"> + Continue <i class="fa fa-chevron-right ml-1"/> + </button> + </div> + </div> + </div> + </t> + + <t t-name="slide.slide.quiz.finish"> + <div> + <button type="button" class="o_wslides_quiz_modal_close_btn close position-absolute" data-dismiss="modal" aria-label="Close">×</button> + <div class="o_wslides_gradient d-none d-md-flex flex-shrink-0"> + <img class="o_wslides_quiz_modal_hero" src="/website_slides/static/src/img/quiz_modal_success.svg" alt=""/> + </div> + <div class="d-flex flex-column flex-grow-1 justify-content-between pl-md-5 p-3 overflow-auto"> + <div> + <h1 class="o_wslides_quiz_modal_title mt-3 display-4 font-weight-bold">Amazing!</h1> + <div class="pb-3"> + <h4 class="o_wslides_quiz_modal_xp_gained pb-2 d-flex fade">You gained <span class="badge badge-pill badge-success text-white font-weight-bold ml-2 mr-1"><t t-esc="widget.quiz.quizKarmaWon"/> XP</span> !</h4> + <div class="mt-5 mb-4"> + <div class="progress"> + <div class="progress-bar" role="progressbar" t-att-aria-valuenow="widget.quiz.rankProgress.previous_rank.progress" aria-valuemin="0" aria-valuemax="100" + t-attf-style="width: #{widget.quiz.rankProgress.previous_rank.progress}%"/> + <div class="progress-bar-tooltip" data-toggle="tooltip" data-placement="top" t-att-title="widget.quiz.rankProgress.new_rank.karma" /> + </div> + <small class="float-left text-primary font-weight-bold o_wslides_quiz_modal_rank_lower_bound"> + <t t-esc="widget.quiz.rankProgress.previous_rank.lower_bound"/> + </small> + <small t-if="widget.quiz.rankProgress.previous_rank.upper_bound" class="float-right font-weight-bold o_wslides_quiz_modal_rank_upper_bound"> + <t t-esc="widget.quiz.rankProgress.previous_rank.upper_bound"/> + </small> + </div> + </div> + <div class="pb-3 o_wslides_quiz_modal_rank_motivational"> + <t t-set="showLastRankDescription" t-value="widget.quiz.rankProgress.last_rank && !widget.quiz.rankProgress.level_up" /> + <t t-raw="showLastRankDescription ? widget.quiz.rankProgress.description : widget.quiz.rankProgress.previous_rank.motivational" /> + </div> + </div> + <div class="o_wslides_quiz_modal_dismiss align-self-end d-none"> + <t t-if="widget.quiz.rankProgress.level_up"> + <a type="button" target="_blank" t-attf-href="/profile/user/#{widget.userId}" class="btn btn-light border">Check Profile</a> + </t> + <t t-if="widget.hasNext"> + <button type="button" class="btn btn-light border o_wslides_quiz_modal_btn">Next <i class="fa fa-chevron-right"/></button> + </t> + <t t-else=""> + <a type="button" href="/slides" class="btn btn-light border">End course</a> + </t> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/slide_quiz_create.xml b/addons/website_slides/static/src/xml/slide_quiz_create.xml new file mode 100644 index 00000000..d9cf7de0 --- /dev/null +++ b/addons/website_slides/static/src/xml/slide_quiz_create.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="slide.quiz.question.input"> + <div t-attf-class="o_wsildes_quiz_question_input mt-3 #{!widget.update ? 'col' : ''}" t-att-data-id="widget.question.id || ''"> + <form class="mb-3"> + <div class="o_wslides_quiz_question row align-items-center mr-0 mb-2"> + <div class="input-group ml-3"> + <div class="input-group-prepend"> + <span class="input-group-text o_wslides_quiz_question_sequence"><t t-esc="widget.sequence"/></span> + </div> + <input type="text" name="question-name" class="form-control col-11" placeholder="Enter your question" + t-att-value="widget.question.text"/> + </div> + </div> + <div class="text-muted mb-2"> + <span>Select the correct answer below :</span> + </div> + <div class="list-group"> + <t t-if="widget.question.answers" > + <t t-foreach="widget.question.answers" t-as="answer" > + <t t-call="slide.quiz.answer.line"/> + </t> + </t> + <t t-else="" > + <t t-foreach="[1, 2, 3]"> + <t t-call="slide.quiz.answer.line" /> + </t> + </t> + </div> + </form> + <t t-if="widget.update" t-call="slide.quiz.update.buttons"/> + <t t-else="" t-call="slide.quiz.create.buttons"/> + </div> + </t> + + <t t-name="slide.quiz.answer.line"> + <div class="o_wslides_js_quiz_answer row align-items-center mb-1" t-attf-data-answer-id="#{answer ? answer.id : ''}" > + <div class="col ml-3 ml-md-5"> + <div class="row align-items-center"> + <div class="input-group col-9 p-0"> + <input type="text" class="o_wslides_js_quiz_answer_value form-control" placeholder="Enter your answer" t-attf-value="#{answer ? answer.text_value : ''}"/> + <div class="input-group-append"> + <div class="input-group-text"> + <a class="o_wslides_js_quiz_is_correct" title="This is the correct answer"> + <label class="my-0"> + <input t-if="answer and answer.is_correct" class="d-none" type="radio" name="radio" checked="true" /> + <input t-else="" class="d-none" type="radio" name="radio" /> + <i class="o_wslides_js_quiz_icon fa fa-lg fa-check-circle-o text-muted" /> + </label> + </a> + </div> + </div> + </div> + <i t-attf-class="o_wslides_js_quiz_icon o_wslides_js_quiz_comment_answer fa fa-lg fa-info-circle p-md-2 py-2 pl-2 pr-1 #{answer && answer.comment ? 'text-primary' : 'text-muted'}" title="Add comment on this answer" /> + <i class="o_wslides_js_quiz_icon o_wslides_js_quiz_add_answer fa fa-lg fa-plus-circle p-md-2 py-2 px-1 text-muted" title="Add an answer below this one" /> + <i class="o_wslides_js_quiz_icon o_wslides_js_quiz_remove_answer fa fa-lg fa-trash-o p-md-2 py-2 px-1 text-muted" title="Remove this answer" /> + </div> + <div class="o_wslides_js_quiz_answer_comment row align-items-center d-none"> + <input type="text" class="form-control col-8 offset-1 mt-1" placeholder="This is the correct answer, congratulations" + t-attf-value="#{answer ? answer.comment : ''}" /> + <i class="o_wslides_js_quiz_icon o_wslides_js_quiz_remove_answer_comment fa fa-lg fa-trash-o p-2 text-muted" title="Remove the answer comment" /> + </div> + </div> + </div> + </t> + + <t t-name="slide.quiz.create.buttons"> + <div> + <a class="o_wslides_js_quiz_validate_question btn btn-primary text-white border" role="button"> + <span>Save</span> + </a> + <a class="o_wslides_js_quiz_cancel_question btn btn-light border" role="button"> + <span>Cancel</span> + </a> + </div> + </t> + + <t t-name="slide.quiz.update.buttons"> + <a class="o_wslides_js_quiz_validate_question o_wslides_js_quiz_update btn btn-primary text-white border" role="button"> + <span>Update</span> + </a> + <a class="o_wslides_js_quiz_cancel_question btn btn-light border" role="button"> + <span>Cancel</span> + </a> + </t> + + <t t-name="slide.quiz.confirm.deletion"> + <div>Are you sure you want to delete this question : <strong t-esc="widget.questionTitle"/> ?</div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/website_slides_channel.xml b/addons/website_slides/static/src/xml/website_slides_channel.xml new file mode 100644 index 00000000..80f02148 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_channel.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates xml:space="preserve"> + + <t t-name="website.slide.channel.create"> + <div> + <form action="/slides/channel/add" method="POST" id="slide_channel_add_form"> + <input type="hidden" name="csrf_token" t-att-value="csrf_token"/> + <div class="form-group"> + <label for="title" class="col-form-label">Title</label> + <input type="text" class="form-control" name="name" id="title" placeholder="Computer Science for kids" required="1"/> + <p id="title-required" class="text-danger mt-1 mb-0 d-none">Please fill in this field</p> + </div> + <div class="form-group"> + <label for="tag_ids" class="col-form-label">Tags</label> + <input type="text" class="form-control" name="tag_ids" id="tag_ids" placeholder="Tags"/> + </div> + <label for="channel_type">Choose a layout</label> + <div class="form-row"> + <div class="form-group col-6"> + <div class="form-check px-0"> + <input class="form-check-input d-none" type="radio" name="channel_type" id="channel_type1" value="training" checked="checked"/> + <label for="channel_type1"> + <img class="w-100" src="/website_slides/static/src/img/channel-training-layout.png" alt="Training Layout"/> + </label> + </div> + </div> + <div class="form-group col-6"> + <div class="form-check px-0"> + <input class="form-check-input d-none" type="radio" name="channel_type" id="channel_type2" value="documentation"/> + <label for="channel_type2"> + <img class="w-100" src="/website_slides/static/src/img/channel-documentation-layout.png" alt="Documentation Layout"/> + </label> + </div> + </div> + </div> + <div class="form-group"> + <label for="title">Description</label> + <textarea rows="2" class="form-control" name="description" id="description" + placeholder="Common tasks for a computer scientist is asking the right questions and answering + questions. In this course, you'll study those topics with activities about mathematics, science and logic." /> + </div> + <div class="form-group"> + <label id="communication-label">Review</label> + <div class="o_wslide_channel_communication_type"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" id="allow_comment" name="allow_comment" checked="checked"/> + <span class="form-check-label" for="allow_comment">Allow students to review your course</span> + </div> + </div> + </div> + </form> + </div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/website_slides_channel_tag.xml b/addons/website_slides/static/src/xml/website_slides_channel_tag.xml new file mode 100644 index 00000000..081032e0 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_channel_tag.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates xml:space="preserve"> + + <t t-name="website.slides.tag.add"> + <div class="form-group"> + <form action="/slides/channel/tag/add" method="POST" id="slides_channel_tag_add_form"> + <div class="form-group"> + <label for="tag_id" class="col-form-label">Tag</label> + <input class="form-control" id="tag_id" required="required"/> + </div> + <div class="form-group"> + <label id="tag_group_label" for="tag_group_id" class="col-form-label">Tag Group</label> + <input class="form-control" id="tag_group_id"/> + </div> + </form> + </div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml new file mode 100644 index 00000000..ceebb9d2 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml @@ -0,0 +1,22 @@ +<templates id="template" xml:space="preserve"> + + <t t-name="website.slides.fullscreen.content"> + <t t-if="_.contains(['document', 'presentation'], widget.get('slide').type)"> + <div class="embed-responsive h-100"> + <iframe t-att-src="widget.get('slide').embedUrl" class="o_wslides_iframe_viewer" allowFullScreen="true" frameborder="0"/> + </div> + </t> + <t t-if="widget.get('slide').type === 'infographic'"> + <div class="o_wslides_fs_player w-100 h-100 overflow-auto d-flex align-items-start justify-content-center"> + <img t-att-src="'/web/image/slide.slide/'+ widget.get('slide').id +'/image_1024'" class="img-fluid position-relative m-auto" alt="Slide image"/> + </div> + </t> + </t> + + <t t-name="website.slides.fullscreen.video"> + <div class="player embed-responsive embed-responsive-16by9 embed-responsive-item h-100"> + <iframe t-att-id="'youtube-player' + widget.slide.id" t-att-src="widget.slide.embedUrl" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe> + </div> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/website_slides_share.xml b/addons/website_slides/static/src/xml/website_slides_share.xml new file mode 100644 index 00000000..6f7792fb --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_share.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="website.slide.share.modal"> + <div> + <t t-call="website.slide.share.socialmedia"/> + </div> + </t> + + <t t-name="website.slide.share.socialmedia"> + <div class="row"> + <div class="col-12 col-lg-6 mb-4"> + <h5 class="mt-0 mb-2">Share on Social Networks</h5> + <div class="btn-group" role="group"> + <a t-attf-href="https://www.facebook.com/sharer/sharer.php?u=#{window.location.href}" class="btn border bg-white o_wslides_js_social_share" social-key="facebook" aria-label="Share on Facebook" title="Share on Facebook"><i class="fa fa-facebook-square fa-fw"/></a> + <a t-attf-href="https://twitter.com/intent/tweet?text=#{widget.slide.name}&url=#{window.location.href}" class="btn border bg-white o_wslides_js_social_share" social-key="twitter" aria-label="Share on Twitter" title="Share on Twitter"><i class="fa fa-twitter fa-fw"/></a> + <a t-attf-href="http://www.linkedin.com/sharing/share-offsite/?url=#{window.location.href}" social-key="linkedin" class="btn border bg-white o_wslides_js_social_share" aria-label="Share on LinkedIn" title="Share on LinkedIn"><i class="fa fa-linkedin fa-fw"/></a> + </div> + </div> + <div class="col-12 col-lg-6"> + <h5 class="mt-0 mb-2">Share Link</h5> + <div class="input-group"> + <input type="text" class="form-control o_wslides_js_share_link" t-att-value="window.location.href" readonly="readonly" onClick="this.select();" /> + <div class="input-group-append"> + <button class="btn btn-sm btn-primary o_clipboard_button" style="border-top-right-radius: 4px;border-bottom-right-radius: 4px;" > + <span class="fa fa-clipboard"> Copy Link</span> + </button> + </div> + </div> + </div> + <div t-attf-class="col-12 col-lg-6"> + <t t-call="website.slide.share.email"/> + </div> + </div> + </t> + + <t t-name="website.slide.share.email"> + <h5 class="mt-4">Share by mail</h5> + <div t-if="!widget.session.is_website_user" class="form-inline"> + <form class="form-group o_wslides_js_share_email" role="form"> + <div class="input-group"> + <input type="email" class="form-control" placeholder="your-friend@domain.com"/> + <span class="input-group-append"> + <button class="btn btn-primary" type="button" + data-loading-text="Sending..." + t-attf-data-slide-id="#{widget.slide.id}" + style="border-top-right-radius: 4px;border-bottom-right-radius: 4px;"> + <i class="fa fa-envelope-o"/> Send Email + </button> + </span> + </div> + </form> + </div> + <div t-if="widget.session.is_website_user" class="alert alert-info d-inline-block"> + <p class="mb-0">Please <a t-attf-href="/web?redirect=#{window.location.href}" class="font-weight-bold"> login </a> to share this <t t-esc="widget.slide.type"/> by email.</p> + </div> + </t> + +</templates>
\ No newline at end of file diff --git a/addons/website_slides/static/src/xml/website_slides_unsubscribe.xml b/addons/website_slides/static/src/xml/website_slides_unsubscribe.xml new file mode 100644 index 00000000..c27c2b89 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_unsubscribe.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="slides.course.unsubscribe.modal"> + <div> + <div class="o_w_slide_unsubscribe_modal_container"> + <t t-call="slides.course.unsubscribe.modal.subscription"/> + </div> + </div> + </t> + + <t t-name="slides.course.unsubscribe.modal.subscription"> + <form class="clearfix"> + <div class="form-group row"> + <div class="controls mt8 ml-3"> + <input id="subscribed" name="subscribed" type="checkbox"/> + <label for="subscribed" class="col-form-label font-weight-normal">Be notified when a new content is added.</label> + </div> + </div> + </form> + </t> + + <t t-name="slides.course.unsubscribe.modal.leave"> + <p>Do you really want to leave the course?</p> + <p>All completed classes and earned karma will be lost.</p> + </t> + +</templates> diff --git a/addons/website_slides/static/src/xml/website_slides_upload.xml b/addons/website_slides/static/src/xml/website_slides_upload.xml new file mode 100644 index 00000000..c517b320 --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_upload.xml @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="website.slide.upload.modal"> + <div> + <div class="o_w_slide_upload_modal_container"> + <t t-call="website.slide.upload.modal.select"/> + </div> + </div> + </t> + + <!-- + Slide Type Selection template + --> + <t t-name="website.slide.upload.modal.select"> + <div class="row p-1 mt-4"> + <div t-foreach="widget.slide_type_data" t-as="slide_type" class="col-6 col-md-3"> + <t t-set="type_data" t-value="widget.slide_type_data[slide_type]"/> + + <a href="#" t-att-data-slide-type="slide_type" + class="content-type d-flex flex-column align-items-center mb-4 o_wslides_select_type btn rounded border text-600 p-3"> + <i t-attf-class="fa #{type_data['icon']} mb-2 fa-3x"/> + <t t-esc="type_data['label']"/> + </a> + </div> + </div> + <t t-if="widget.modulesToInstall"> + <t t-foreach="widget.modulesToInstall" t-as="module_info"> + <a class="o_wslides_js_upload_install_button w-100 text-center mb-4 btn rounded border text-600 p-3" + href="#" t-att-title="module_info['name']" + t-att-data-module-id="module_info['id']"> + <i class="fa fa-trophy"></i> <t t-esc="module_info['motivational']"/> + </a> + </t> + </t> + </t> + + <!-- + Uploading template + --> + <t t-name="website.slide.upload.modal.uploading"> + <div class="text-center" role="status"> + <div class="fa-3x"> + <i class="fa fa-spinner fa-pulse"></i> + </div> + <h4>Uploading document ...</h4> + </div> + </t> + + <!-- + Import module template + --> + <t t-name="website.slide.upload.modal.import"> + <p id="o_wslides_install_module_text"/> + </t> + + <!-- + Slide Type common form part template + --> + <t t-name="website.slide.upload.modal.common"> + <div class="form-group"> + <label for="name" class="col-form-label">Title</label> + <input id="name" name="name" placeholder="Title" class="form-control" required="required"/> + </div> + <div t-if="!widget.defaultCategoryID" class="form-group"> + <label for="category_id" class="col-form-label">Section</label> + <input class="form-control" id="category_id"/> + </div> + <div class="form-group"> + <label for="tag_ids" class="col-form-label">Tags</label> + <input id="tag_ids" name="tag_ids" type="hidden"/> + </div> + <div class="form-group"> + <label for="duration" class="col-form-label">Duration</label> + <div class="input-group"> + <input type="number" id="duration" min="0" name="duration" placeholder="Estimated slide completion time" class="form-control"/> + <div class="input-group-prepend"> + <span class="input-group-text">Minutes</span> + </div> + </div> + </div> + </t> + + <!-- + Slide Type templates + --> + <t t-name="website.slide.upload.modal.presentation"> + <div> + <form class="clearfix"> + <div class="row"> + <div id="o_wslides_js_slide_upload_left_column" class="col-md-6"> + <div class="form-group"> + <label for="upload" class="col-form-label">Choose a PDF or an Image</label> + <input id="upload" name="file" class="form-control h-100" accept="image/*,application/pdf" type="file" required="required"/> + </div> + <canvas id="data_canvas" class="d-none"></canvas> + <t t-call="website.slide.upload.modal.common"/> + </div> + <div id="o_wslides_js_slide_upload_preview_column" class="col-md-6"> + <div class="img-thumbnail h-100"> + <div class="o_slide_tutorial p-3"> + <div class="h5">How to upload your PowerPoint Presentations or Word Documents?</div> + <div class="mx-3 my-4">Save your presentations or documents as PDF files and upload them.</div> + <div class="alert alert-warning" role="alert"> + <i class="fa fa-info-circle pr-2"/> + Only JPG, PNG, PDF, files types are supported + </div> + </div> + <div class="o_slide_preview d-none"> + <img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" alt="Content Preview" class="img-fluid"/> + </div> + </div> + </div> + </div> + </form> + </div> + </t> + + <t t-name="website.slide.upload.modal.webpage"> + <div> + <form class="clearfix"> + <div class="row"> + <div id="o_wslides_js_slide_upload_left_column" class="col-md-6"> + <canvas id="data_canvas" class="d-none"></canvas> + <t t-call="website.slide.upload.modal.common"/> + </div> + <div id="o_wslides_js_slide_upload_preview_column" class="col-md-6"> + <div class="img-thumbnail h-100"> + <div class="o_slide_tutorial p-3"> + <div class="h5">How to create a Lesson as a Web Page?</div> + <div class="mx-3 my-4">First, create your lesson, then edit it with the website builder. You'll be able to drop building blocks on your page and edit them.</div> + </div> + </div> + </div> + </div> + </form> + </div> + </t> + + <t t-name="website.slide.upload.modal.video"> + <div> + <form class="clearfix"> + <div class="row"> + <div id="o_wslides_js_slide_upload_left_column" class="col-md-6"> + <div class="form-group"> + <label for="url" class="col-form-label">Youtube Link</label> + <input id="url" name="url" class="form-control" placeholder="Youtube Video URL" required="required"/> + </div> + <canvas id="data_canvas" class="d-none"></canvas> + <t t-call="website.slide.upload.modal.common"/> + </div> + <div id="o_wslides_js_slide_upload_preview_column" class="col-md-6"> + <div class="img-thumbnail h-100"> + <div class="o_slide_tutorial p-3"> + <div class="h5">How to upload your videos ?</div> + <div class="mx-3 my-4">First, upload your videos on YouTube and mark them as <strong>unlisted</strong>. This way, they will be secured.</div> + <div class="mx-3 my-4">What does <strong>unlisted</strong> means? The YouTube "unlisted" means it is a video which can be viewed only by the users with the link to it. Your video will never come up in the search results nor on your channel.</div> + <div class="mx-3 my-4"><a href="https://support.google.com/youtube/answer/157177" target="_blank" >Change video privacy settings</a></div> + </div> + <div class="o_slide_preview d-none"> + <img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" alt="Content Preview" class="img-fluid"/> + </div> + </div> + </div> + </div> + </form> + </div> + </t> + + <t t-name="website.slide.upload.quiz"> + <div> + <form class="clearfix"> + <div class="row"> + <div id="o_wslides_js_slide_upload_left_column" class="col-md-6"> + <canvas id="data_canvas" class="d-none"></canvas> + <t t-call="website.slide.upload.modal.common"/> + </div> + <div id="o_wslides_js_slide_upload_preview_column" class="col-md-6"> + <div class="img-thumbnail h-100"> + <div class="o_slide_tutorial p-3"> + <div class="h5">Test your students with small Quizzes</div> + <div class="mx-3 my-4">With Quizzes you can keep your students focused and motivated by answering some questions and gaining some karma points</div> + <img src="/website_slides/static/src/img/onboarding-quiz.png" title="Quiz Demo Data" class="img-fluid"/> + </div> + </div> + </div> + </div> + </form> + </div> + </t> +</templates> |
