diff options
| author | Aurélien Warnon <awa@odoo.com> | 2019-02-11 14:03:02 +0000 |
|---|---|---|
| committer | Aurélien Warnon <awa@odoo.com> | 2019-02-15 15:32:46 +0000 |
| commit | f322816ae826f14eafd6f1d4855b0e2a741530d5 (patch) | |
| tree | 77176562e6128b65275b9096a0e278789b122f75 | |
| parent | dcce7716b3202eabd77666bcaaee9040e6a5e5b8 (diff) | |
[ADD] website_slides_survey: add a new bridge between slide and survey
Task #1940360
Subtask of #1902304
Purpose
=======
Adds certification capabilities to the website_slides module.
Channel/courses can now include certifications as a new type of slide.
This new type of slide is available as a "Certification" button on the slide creation
frontend view (next to "Video", "Presentation", ...).
Users have to link the slide to an actual survey that has the 'certificate' field
set to true (that will populate the slide's survey_id field).
Slides of type certification are handled in frontend in a very simple way for now:
- A button "Begin certification" that redirects the user to the related survey frontend.
- A button "Download certification" when the user has succeeded the certification.
- When the survey is done, if it's linked to a slide, a button "Go back to course" allows
the user to go back to the slide frontend.
(There is a special use case for when the website_publisher designing the survey lands on a
certification slide: he is allowed to test the survey with a survey_input created as test_entry)
Survey creation as well as limited time, limited number of attempts, ... are still completely
handled in the survey module. That means that the user will have to first create a suitable survey
that is a certification and only then create a slide of type "certification" and link
the created survey to it.
Ideally, the taking of the survey should be transparently included in the slide frontend but
it requires a full refactoring of the way surveys are submitted. This will most likely come in
a later commit.
This commit is an advanced merge of full eLearning module (see task #1902304).
35 files changed, 757 insertions, 49 deletions
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 1fca997541f..446caf6a55e 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -136,7 +136,7 @@ class Survey(http.Controller): elif error_key == 'answer_deadline' and answer_sudo.token: return request.render("survey.survey_expired", {'survey': survey_sudo}) elif error_key == 'answer_done' and answer_sudo.token: - return request.render("survey.sfinished", {'survey': survey_sudo, 'token': answer_sudo.token, 'answer': answer_sudo}) + return request.render("survey.sfinished", self._prepare_survey_finished_values(survey_sudo, answer_sudo, token=answer_sudo.token)) return werkzeug.utils.redirect("/") @@ -165,14 +165,23 @@ class Survey(http.Controller): return werkzeug.utils.redirect("/") try: - retry_answer_sudo = survey_sudo._create_answer(user=request.env.user, partner=answer_sudo.partner_id, email=answer_sudo.email, invite_token=answer_sudo.invite_token, **{ - 'input_type': answer_sudo.input_type, - 'deadline': answer_sudo.deadline, - }) + retry_answer_sudo = survey_sudo._create_answer( + user=request.env.user, + partner=answer_sudo.partner_id, + email=answer_sudo.email, + invite_token=answer_sudo.invite_token, + **self._prepare_retry_additional_values(answer_sudo) + ) except: return werkzeug.utils.redirect("/") return request.redirect('/survey/start/%s?%s' % (survey_sudo.access_token, keep_query('*', answer_token=retry_answer_sudo.token))) + def _prepare_retry_additional_values(self, answer): + return { + 'input_type': answer.input_type, + 'deadline': answer.deadline, + } + @http.route('/survey/start/<string:survey_token>', type='http', auth='public', website=True) def survey_start(self, survey_token, answer_token=None, email=False, **post): """ Start a survey by providing @@ -256,8 +265,7 @@ class Survey(http.Controller): data.update({'last': True}) return request.render('survey.survey', data) elif answer_sudo.state == 'done': # Display success message - return request.render('survey.sfinished', {'survey': survey_sudo, - 'answer': answer_sudo}) + return request.render('survey.sfinished', self._prepare_survey_finished_values(survey_sudo, answer_sudo)) elif answer_sudo.state == 'skip': flag = (True if prev and prev == 'prev' else False) page_or_question_id, last = survey_sudo.next_page_or_question(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=flag) @@ -627,3 +635,9 @@ class Survey(http.Controller): 'success_rate': round((quizz_passed_count / total_quizz_passed) * 100, 1) if total_quizz_passed > 0 else 0, 'graph_data': graph_data } + + def _prepare_survey_finished_values(self, survey, answer, token=False): + values = {'survey': survey, 'answer': answer} + if token: + values['token'] = token + return values diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py index bf79968b381..86d810ca588 100644 --- a/addons/survey/models/survey_user.py +++ b/addons/survey/models/survey_user.py @@ -145,6 +145,11 @@ class SurveyUserInput(models.Model): if user_input.survey_id.certificate and user_input.quizz_passed and user_input.survey_id.certification_mail_template_id: user_input.survey_id.certification_mail_template_id.send_mail(user_input.id, notif_layout="mail.mail_notification_light") + @api.multi + def _get_survey_url(self): + self.ensure_one() + return '/survey/start/%s?answer_token=%s' % (self.survey_id.access_token, self.token) + class SurveyUserInputLine(models.Model): _name = 'survey.user_input_line' diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index df122e9d90c..6a75730daa0 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -115,15 +115,19 @@ class WebsiteSlides(WebsiteProfile): # fetch 'latests achievements' for non logged people if request.env.user._is_public(): - achievements = request.env['gamification.badge.user'].sudo().search([], limit=5) + achievements = request.env['gamification.badge.user'].sudo().search([('badge_id.is_published', '=', True)], limit=5) challenges = None challenges_done = None else: achievements = None - challenges = request.env['gamification.challenge'].sudo().search([('category', '=', 'slides')], order='id asc', limit=5) + challenges = request.env['gamification.challenge'].sudo().search([ + ('category', '=', 'slides'), + ('reward_id.is_published', '=', True) + ], order='id asc', limit=5) challenges_done = request.env['gamification.badge.user'].sudo().search([ ('challenge_id', 'in', challenges.ids), - ('user_id', '=', request.env.user.id) + ('user_id', '=', request.env.user.id), + ('badge_id.is_published', '=', True) ]).mapped('challenge_id') # fetch 'heroes of the week' for non logged people @@ -325,8 +329,8 @@ class WebsiteSlides(WebsiteProfile): if not slide.channel_id.can_access_from_current_website(): raise werkzeug.exceptions.NotFound() - values = self._get_slide_detail(slide) self._set_viewed_slide(slide) + values = self._get_slide_detail(slide) # allow rating and comments if slide.channel_id.allow_comment: values.update({ @@ -422,9 +426,7 @@ class WebsiteSlides(WebsiteProfile): if (file_size / 1024.0 / 1024.0) > 25: return {'error': _('File is too big. File size cannot exceed 25MB')} - values = dict((fname, post[fname]) for fname in [ - 'name', 'url', 'tag_ids', 'slide_type', 'channel_id', - 'mime_type', 'datas', 'description', 'image', 'index_content', 'website_published'] if post.get(fname)) + values = dict((fname, post[fname]) for fname in self._get_valid_slide_post_values() if post.get(fname)) if post.get('category_id'): if post['category_id'][0] == 0: @@ -465,6 +467,10 @@ class WebsiteSlides(WebsiteProfile): redirect_url += "?enable_editor=1" return {'url': redirect_url} + def _get_valid_slide_post_values(self): + return ['name', 'url', 'tag_ids', 'slide_type', 'channel_id', + 'mime_type', 'datas', 'description', 'image', 'index_content', 'website_published'] + @http.route(['/slides/channel/tag/search_read'], type='json', auth='user', methods=['POST'], website=True) def slide_channel_tag_search_read(self, fields, domain): can_create = request.env['slide.channel.tag'].check_access_rights('create', raise_exception=False) diff --git a/addons/website_slides/data/gamification_data.xml b/addons/website_slides/data/gamification_data.xml index 3a82e567393..4bf7a829b9d 100644 --- a/addons/website_slides/data/gamification_data.xml +++ b/addons/website_slides/data/gamification_data.xml @@ -138,14 +138,14 @@ <field name="description">Get a certification</field> <field name="computation_mode">count</field> <field name="display_mode">boolean</field> - <field name="model_id" eval="ref('website_slides.model_slide_channel_partner')"/> + <field name="model_id" eval="ref('website_slides.model_slide_slide_partner')"/> <field name="condition">higher</field> <field name="domain">[ ('completed', '=', True), (0, '=', 1) ]</field> <field name="batch_mode">True</field> - <field name="batch_distinctive_field" eval="ref('website_slides.field_slide_channel_partner__partner_id')"/> + <field name="batch_distinctive_field" eval="ref('website_slides.field_slide_slide_partner__partner_id')"/> <field name="batch_user_expression">user.partner_id.id</field> </record> <record id="badge_data_certification_challenge" model="gamification.challenge"> diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index c94cda59b8c..ead43565bcb 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -157,9 +157,7 @@ class Channel(models.Model): @api.depends('slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views') def _compute_slides_statistics(self): - result = dict.fromkeys(self.ids, dict( - nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0, - total_slides=0, total_views=0, total_votes=0, total_time=0)) + result = dict.fromkeys(self.ids, dict(total_slides=0, total_views=0, total_votes=0, total_time=0)) read_group_res = self.env['slide.slide'].read_group( [('is_published', '=', True), ('channel_id', 'in', self.ids)], ['channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time'], @@ -167,19 +165,31 @@ class Channel(models.Model): lazy=False) for res_group in read_group_res: cid = res_group['channel_id'][0] - result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0 - result[cid]['nbr_documents'] += res_group.get('slide_type', '') == 'document' and res_group['__count'] or 0 - result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0 - result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0 - result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0 result[cid]['total_slides'] += res_group['__count'] result[cid]['total_views'] += res_group.get('total_views', 0) result[cid]['total_votes'] += res_group.get('likes', 0) result[cid]['total_votes'] -= res_group.get('dislikes', 0) result[cid]['total_time'] += res_group.get('completion_time', 0) + + type_stats = self._compute_slides_statistics_type(read_group_res) + for cid, cdata in type_stats.items(): + result[cid].update(cdata) + for record in self: record.update(result[record.id]) + def _compute_slides_statistics_type(self, read_group_res): + """ Can be overridden to compute stats on added slide_types """ + result = dict.fromkeys(self.ids, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0)) + for res_group in read_group_res: + cid = res_group['channel_id'][0] + result[cid]['nbr_presentations'] += res_group.get('slide_type', '') == 'presentation' and res_group['__count'] or 0 + result[cid]['nbr_documents'] += res_group.get('slide_type', '') == 'document' and res_group['__count'] or 0 + result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0 + result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0 + result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0 + return result + @api.depends('slide_partner_ids') def _compute_user_statistics(self): current_user_info = self.env['slide.channel.partner'].sudo().search( @@ -382,10 +392,25 @@ class Category(models.Model): lazy=False) for res_group in res: result[res_group['category_id'][0]][res_group['slide_type']] = result[res_group['category_id'][0]].get(res_group['slide_type'], 0) + res_group['__count'] + for record in self: - record.nbr_presentations = result[record.id].get('presentation', 0) - record.nbr_documents = result[record.id].get('document', 0) - record.nbr_videos = result[record.id].get('video', 0) - record.nbr_infographics = result[record.id].get('infographic', 0) - record.nbr_webpages = result[record.id].get('webpage', 0) - record.total_slides = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics + record.nbr_webpages + record.update(self._extract_count_presentations_type(result, record.id)) + + def _extract_count_presentations_type(self, result, record_id): + """ Can be overridden to compute stats on added slide_types """ + statistics = { + 'nbr_presentations': result[record_id].get('presentation', 0), + 'nbr_documents': result[record_id].get('document', 0), + 'nbr_videos': result[record_id].get('video', 0), + 'nbr_infographics': result[record_id].get('infographic', 0), + 'nbr_webpages': result[record_id].get('webpage', 0), + 'total_slides': 0 + } + + statistics['total_slides'] += statistics['nbr_presentations'] + statistics['total_slides'] += statistics['nbr_documents'] + statistics['total_slides'] += statistics['nbr_videos'] + statistics['total_slides'] += statistics['nbr_infographics'] + statistics['total_slides'] += statistics['nbr_webpages'] + + return statistics diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 5ac61bbec39..5dc5dcc4d5a 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -126,6 +126,8 @@ class Slide(models.Model): partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', string='Subscribers', groups='base.group_website_publisher') slide_partner_ids = fields.One2many('slide.slide.partner', 'slide_id', string='Subscribers information', groups='base.group_website_publisher') + user_membership_id = fields.Many2one('slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', + help="Subscriber information for the current logged in user") # content slide_type = fields.Selection([ ('infographic', 'Infographic'), @@ -191,6 +193,19 @@ class Slide(models.Model): for slide in self: slide.slide_views = mapped_data.get(slide.id, 0) + @api.depends('slide_partner_ids.partner_id') + def _compute_user_membership_id(self): + slide_partners = self.env['slide.slide.partner'].sudo().search([ + ('slide_id', 'in', self.ids), + ('partner_id', '=', self.env.user.partner_id.id), + ]) + + for record in self: + record.user_membership_id = next( + (slide_partner for slide_partner in slide_partners if slide_partner.slide_id == record), + self.env['slide.slide.partner'] + ) + def _get_embed_code(self): base_url = request and request.httprequest.url_root or self.env['ir.config_parameter'].sudo().get_param('web.base.url') if base_url[-1] == '/': diff --git a/addons/website_slides/static/src/js/slides.js b/addons/website_slides/static/src/js/slides.js index 8692dd67a46..bda80f1e689 100644 --- a/addons/website_slides/static/src/js/slides.js +++ b/addons/website_slides/static/src/js/slides.js @@ -33,6 +33,9 @@ sAnimations.registry.websiteSlides = sAnimations.Class.extend({ return $.when.apply($, defs); }, }); + +return sAnimations.registry.websiteSlides; + }); //============================================================================== @@ -120,4 +123,5 @@ sAnimations.registry.websiteSlidesEmbed = sAnimations.Class.extend({ new SlideSocialEmbed(this, maxPage).attachTo($('.oe_slide_js_embed_code_widget')); }, }); + }); diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 30901d80640..855b7e52932 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -150,11 +150,17 @@ var SlideUploadDialog = Dialog.extend({ 'image': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data, }); } else if (/^image\/.*/.test(this.file.type)) { - _.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 - }); + 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': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data, + }); + } } return values; }, @@ -229,15 +235,18 @@ var SlideUploadDialog = Dialog.extend({ * @param {bool} true for multiple selection box, false for single selection * @param {Function} Function to fetch data from remote location should return $.deferred object * 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) { - return { + _select2Wrapper: function (tag, multi, fetchFNC, nameKey) { + nameKey = nameKey || 'name'; + + var values = { width: '100%', placeholder: tag, allowClear: true, formatNoMatches: false, - multiple: multi, selection_data: false, fetch_rpc_fnc: fetchFNC, formatSelection: function (data) { @@ -267,8 +276,8 @@ var SlideUploadDialog = Dialog.extend({ 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}); + if (that.matcher(query.term, obj[nameKey])) { + tags.results.push({id: obj.id, text: obj[nameKey]}); } }); query.callback(tags); @@ -287,6 +296,12 @@ var SlideUploadDialog = Dialog.extend({ } } }; + + if (multi) { + values['multiple'] = true; + } + + return values; }, /** * Init the data relative to the support slide type to upload @@ -550,6 +565,9 @@ sAnimations.registry.websiteSlidesUpload = sAnimations.Class.extend({ }, }); -return SlideUploadDialog; +return { + SlideUploadDialog: SlideUploadDialog, + websiteSlidesUpload: sAnimations.registry.websiteSlidesUpload +}; }); diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index cad8d8ef74d..7e7fcdf92e4 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -47,9 +47,7 @@ </button> </div> <field name="image" widget="image" class="oe_avatar" options='{"preview_image": "image_large"}' - attrs="{ - 'required': [('slide_type', 'not in', ('document'))], - 'readonly': [('slide_type', 'in', ('document', 'presentation', 'video'))]}" + attrs="{'readonly': [('slide_type', 'in', ('document', 'presentation', 'video'))]}" /> <div class="oe_title"> <div class="oe_edit_only"> diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates.xml index a8089a1ef93..af82d23701c 100644 --- a/addons/website_slides/views/website_slides_templates.xml +++ b/addons/website_slides/views/website_slides_templates.xml @@ -392,7 +392,7 @@ <div class="container mt16"> <div class="row"> <div class="col-xl-8 col-lg-8 col-md-12 col-12"> - <div> + <div class="o_w_slides_slide_detail_container"> <t t-if="slide.datas and slide.slide_type == 'infographic'"> <img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-fluid" style="width:100%" alt="Slide image"/> </t> diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml index 1366547b68b..caeab4f77df 100644 --- a/addons/website_slides/views/website_slides_templates_homepage.xml +++ b/addons/website_slides/views/website_slides_templates_homepage.xml @@ -22,13 +22,10 @@ <div class="collapse navbar-collapse" id="navbarSlidesHomepage"> <a class="navbar-brand" href="#"></a> <ul class="navbar-nav mr-auto"> - <li class="nav-item"> + <li class="nav-item o_wslides_home_all_slides"> <h4><a class="nav-link" href="/slides/all"><i class="fa fa-rocket"/> All courses</a></h4> </li> <li class="nav-item ml16"> - <h4><a class="nav-link" href="/slides/all"><i class="fa fa-certificate"/> Certifications</a></h4> - </li> - <li class="nav-item ml16"> <h4><a class="nav-link" href="/slides/all"><i class="fa fa-file"/> My courses</a></h4> </li> </ul> diff --git a/addons/website_slides_survey/__init__.py b/addons/website_slides_survey/__init__.py new file mode 100644 index 00000000000..c3d410ea160 --- /dev/null +++ b/addons/website_slides_survey/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/addons/website_slides_survey/__manifest__.py b/addons/website_slides_survey/__manifest__.py new file mode 100644 index 00000000000..a5e22c395b0 --- /dev/null +++ b/addons/website_slides_survey/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': "Course certifications", + 'summary': 'Add certification capabilities to your courses', + 'description': """This module lets you use the full power of certifications within your courses.""", + 'category': 'Hidden', + 'version': '0.1', + + 'depends': ['website_slides', 'website_survey'], + 'installable': True, + 'auto_install': True, + 'data': [ + 'views/assets.xml', + 'views/slide_channel_views.xml', + 'views/slide_slide_views.xml', + 'views/website_slides_templates.xml', + 'views/website_slides_templates_homepage.xml', + 'views/survey_templates.xml', + 'data/gamification_data.xml', + ], + 'demo': [ + 'data/survey_demo.xml', + 'data/slide_slide_demo.xml', + ], +} diff --git a/addons/website_slides_survey/controllers/__init__.py b/addons/website_slides_survey/controllers/__init__.py new file mode 100644 index 00000000000..232ed200c3c --- /dev/null +++ b/addons/website_slides_survey/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import main +from . import slides +from . import survey diff --git a/addons/website_slides_survey/controllers/main.py b/addons/website_slides_survey/controllers/main.py new file mode 100644 index 00000000000..2a842790a81 --- /dev/null +++ b/addons/website_slides_survey/controllers/main.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request + + +class WebsiteSlidesSurvey(http.Controller): + @http.route(['/slides_survey/certification/search_read'], type='json', auth='user', methods=['POST'], website=True) + def slides_certification_search_read(self, fields): + return { + 'read_results': request.env['survey.survey'].search_read([('certificate', '=', True)], fields), + } diff --git a/addons/website_slides_survey/controllers/slides.py b/addons/website_slides_survey/controllers/slides.py new file mode 100644 index 00000000000..61a55bcecab --- /dev/null +++ b/addons/website_slides_survey/controllers/slides.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.website_slides.controllers.main import WebsiteSlides + +from odoo.http import request + + +class WebsiteSlides(WebsiteSlides): + def _get_valid_slide_post_values(self): + result = super(WebsiteSlides, self)._get_valid_slide_post_values() + result.append('survey_id') + return result + + def _get_slide_detail(self, slide): + """ If the slide is a certification, we pass to the template additional information: + - The certification url, built with the latest attempt (survey.user_input) token + - A 'quizz_passed' boolean to know if we have to show the 'download certification' button + - The 'survey_id' to be able to download the certification + + If the user is not a member of the channel, we give the opportunity to test the certification. + The user still has to have the necessary rights, please check survey.survey._check_answer_creation + for more info. + This is used in the context of a website_publisher designing a course.""" + result = super(WebsiteSlides, self)._get_slide_detail(slide) + if not request.env.user._is_public() and slide.slide_type == 'certification' and slide.survey_id: + if slide.channel_id.is_member: + user_membership_id_sudo = slide.user_membership_id.sudo() + quizz_passed = user_membership_id_sudo.survey_quizz_passed + result['quizz_passed'] = quizz_passed + result['survey_id'] = slide.survey_id.id + if not quizz_passed: + last_user_input = next(user_input for user_input in user_membership_id_sudo.user_input_ids.sorted( + lambda user_input: user_input.create_date, reverse=True + )) + result['certification_url'] = last_user_input._get_survey_url() + else: + result['quizz_passed'] = False + result['test_entry'] = True + user_input = slide.survey_id._create_answer( + partner=request.env.user.partner_id, + check_attempts=False, + test_entry=True, **{ + 'slide_id': slide.id + } + ) + result['certification_url'] = user_input._get_survey_url() + + return result diff --git a/addons/website_slides_survey/controllers/survey.py b/addons/website_slides_survey/controllers/survey.py new file mode 100644 index 00000000000..ce83f61bc8c --- /dev/null +++ b/addons/website_slides_survey/controllers/survey.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.survey.controllers.main import Survey + + +class Survey(Survey): + def _prepare_survey_finished_values(self, survey, answer, token=False): + result = super(Survey, self)._prepare_survey_finished_values(survey, answer, token) + if answer.slide_id: + result['slide_url'] = answer.slide_id.website_url + + return result + + def _prepare_retry_additional_values(self, answer): + result = super(Survey, self)._prepare_retry_additional_values(answer) + if answer.slide_id: + result['slide_id'] = answer.slide_id.id + if answer.slide_partner_id: + result['slide_partner_id'] = answer.slide_partner_id.id + + return result diff --git a/addons/website_slides_survey/data/gamification_data.xml b/addons/website_slides_survey/data/gamification_data.xml new file mode 100644 index 00000000000..8135cabca80 --- /dev/null +++ b/addons/website_slides_survey/data/gamification_data.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo><data noupdate="0"> + <record id="website_slides.badge_data_certification" model="gamification.badge"> + <field name="website_published" eval="True"/> + </record> + <record id="website_slides.badge_data_certification_goal" model="gamification.goal.definition"> + <field name="domain">[ + ('survey_quizz_passed', '=', True), + ('slide_id.slide_type', '=', 'certification') + ]</field> + </record> +</data></odoo> diff --git a/addons/website_slides_survey/data/slide_slide_demo.xml b/addons/website_slides_survey/data/slide_slide_demo.xml new file mode 100644 index 00000000000..90fdc30d889 --- /dev/null +++ b/addons/website_slides_survey/data/slide_slide_demo.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo><data noupdate="0"> + <record id="slide_slide_demo_5_2" model="slide.slide"> + <field name="name">Furniture Creation Certification</field> + <field name="slide_type">certification</field> + <field name="channel_id" ref="website_slides.slide_channel_demo_5_furn2"/> + <field name="survey_id" ref="website_slides_survey.furniture_certification"/> + <field name="website_published" eval="True"/> + <field name="is_preview" eval="False"/> + <field name="image" type="base64" file="website_slides/static/src/img/channel_demo_furniture.jpg"/> + <field name="description">Now that you have completed the course, it's time to test your knowledge!</field> + </record> +</data></odoo> diff --git a/addons/website_slides_survey/data/survey_demo.xml b/addons/website_slides_survey/data/survey_demo.xml new file mode 100644 index 00000000000..8333672c566 --- /dev/null +++ b/addons/website_slides_survey/data/survey_demo.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + <!-- Basics of Furniture Creation Certification --> + <record model="survey.survey" id="furniture_certification"> + <field name="title">Furniture Creation Certification</field> + <field name="access_token">5632a4d7-48cf-4d25-8c52-2174d58cf50b</field> + <field name="stage_id" ref="survey.stage_in_progress" /> + <field name="access_mode">public</field> + <field name="users_can_go_back" eval="True" /> + <field name="users_login_required" eval="True" /> + <field name="scoring_type" >scoring_with_answers</field> + <field name="certificate" eval="True"></field> + <field name="certification_mail_template_id" ref="survey.mail_template_certification"></field> + <field name="is_attempts_limited" eval="True" /> + <field name="attempts_limit">3</field> + <field name="description"><p>Test your furniture knowledge!.</p></field> + <field name="thank_you_message"><p></p></field> + </record> + <!-- Page 1 --> + <record model="survey.question" id="furniture_certification_page_1"> + <field name="title">Furniture</field> + <field name="survey_id" ref="furniture_certification" /> + <field name="sequence">1</field> + <field name="is_page" eval="True"/> + <field name="description"><p>Test your furniture knowledge!</p></field> + </record> + <!-- Question and predefined answer 1 --> + <record model="survey.question" id="furniture_certification_page_1_question_1"> + <field name="survey_id" ref="furniture_certification" /> + <field name="sequence">2</field> + <field name="title">What type of wood is the best for furniture?</field> + <field name="question_type">simple_choice</field> + <field name="display_mode">dropdown</field> + <field name="constr_mandatory" eval="True" /> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_1_choice_1"> + <field name="question_id" ref="furniture_certification_page_1_question_1"/> + <field name="sequence">1</field> + <field name="value">Fir</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_1_choice_2"> + <field name="question_id" ref="furniture_certification_page_1_question_1"/> + <field name="sequence">2</field> + <field name="value">Oak</field> + <field name="is_correct" eval="True" /> + <field name="answer_score">2.0</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_1_choice_3"> + <field name="question_id" ref="furniture_certification_page_1_question_1"/> + <field name="sequence">3</field> + <field name="value">Ash</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_1_choice_4"> + <field name="question_id" ref="furniture_certification_page_1_question_1"/> + <field name="sequence">4</field> + <field name="value">Beech</field> + </record> + <!-- Question and predefined answer 2 --> + <record model="survey.question" id="furniture_certification_page_1_question_2"> + <field name="survey_id" ref="furniture_certification" /> + <field name="sequence">3</field> + <field name="title">Select all the furniture shown in the video</field> + <field name="question_type">multiple_choice</field> + <field name="column_nb">4</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_2_choice_1"> + <field name="question_id" ref="furniture_certification_page_1_question_2"/> + <field name="sequence">1</field> + <field name="value">Chair</field> + <field name="is_correct" eval="True" /> + <field name="answer_score">1.0</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_2_choice_2"> + <field name="question_id" ref="furniture_certification_page_1_question_2"/> + <field name="sequence">2</field> + <field name="value">Table</field> + <field name="answer_score">-1.0</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_2_choice_3"> + <field name="question_id" ref="furniture_certification_page_1_question_2"/> + <field name="sequence">3</field> + <field name="value">Desk</field> + <field name="is_correct" eval="True" /> + <field name="answer_score">1.0</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_2_choice_4"> + <field name="question_id" ref="furniture_certification_page_1_question_2"/> + <field name="sequence">4</field> + <field name="value">Shelve</field> + <field name="is_correct" eval="True" /> + <field name="answer_score">1.0</field> + </record> + <record model="survey.label" id="furniture_certification_page_1_question_2_choice_5"> + <field name="question_id" ref="furniture_certification_page_1_question_2"/> + <field name="sequence">5</field> + <field name="value">Bed</field> + <field name="answer_score">-1.0</field> + </record> + <!-- Question and predefined answer 5 --> + <record model="survey.question" id="furniture_certification_page_1_question_3"> + <field name="survey_id" ref="furniture_certification" /> + <field name="sequence">4</field> + <field name="title">What do you think about the content of the course? (not rated)</field> + <field name="question_type">free_text</field> + </record> + </data> +</odoo> diff --git a/addons/website_slides_survey/models/__init__.py b/addons/website_slides_survey/models/__init__.py new file mode 100644 index 00000000000..f0fb0a0cc82 --- /dev/null +++ b/addons/website_slides_survey/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import slide_slide +from . import slide_channel +from . import survey_user +from . import survey_survey diff --git a/addons/website_slides_survey/models/slide_channel.py b/addons/website_slides_survey/models/slide_channel.py new file mode 100644 index 00000000000..d4132d33151 --- /dev/null +++ b/addons/website_slides_survey/models/slide_channel.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Channel(models.Model): + _inherit = 'slide.channel' + + nbr_certifications = fields.Integer("Number of Certifications", compute='_compute_slides_statistics', store=True) + + def _compute_slides_statistics_type(self, read_group_res): + result = super(Channel, self)._compute_slides_statistics_type(read_group_res) + for cid in result: + result[cid]['nbr_certifications'] = 0 + for res_group in read_group_res: + cid = res_group['channel_id'][0] + result[cid]['nbr_certifications'] += res_group.get('slide_type', '') == 'certification' and res_group['__count'] or 0 + return result + + +class Category(models.Model): + _inherit = 'slide.category' + + nbr_certifications = fields.Integer("Number of Certifications", compute='_count_presentations', store=True) + + def _extract_count_presentations_type(self, result, record_id): + statistics = super(Category, self)._extract_count_presentations_type(result, record_id) + statistics['nbr_certifications'] = result[record_id].get('certification', 0) + statistics['total_slides'] += statistics['nbr_certifications'] + + return statistics diff --git a/addons/website_slides_survey/models/slide_slide.py b/addons/website_slides_survey/models/slide_slide.py new file mode 100644 index 00000000000..5165589533f --- /dev/null +++ b/addons/website_slides_survey/models/slide_slide.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class SlidePartnerRelation(models.Model): + _inherit = 'slide.slide.partner' + + user_input_ids = fields.One2many('survey.user_input', 'slide_partner_id', 'Certification attempts') + survey_quizz_passed = fields.Boolean('Certification Quizz Passed', compute='_compute_survey_quizz_passed', store=True) + + @api.depends('partner_id', 'user_input_ids.quizz_passed') + def _compute_survey_quizz_passed(self): + passed_user_inputs = self.env['survey.user_input'].sudo().search([ + ('slide_partner_id', 'in', self.ids), + ('quizz_passed', '=', True) + ]) + passed_slide_partners = passed_user_inputs.mapped('slide_partner_id') + for record in self: + record.survey_quizz_passed = record in passed_slide_partners + + +class Slide(models.Model): + _inherit = 'slide.slide' + + slide_type = fields.Selection(selection_add=[('certification', 'Certification')]) + survey_id = fields.Many2one('survey.survey', 'Certification') + + _sql_constraints = [ + ('check_survey_id', "CHECK(slide_type != 'certification' OR survey_id IS NOT NULL)", "A slide of type 'certification' requires a certification."), + ('check_certification_preview', "CHECK(slide_type != 'certification' OR is_preview = False)", "A slide of type certification cannot be previewed."), + ] + + def _action_set_viewed(self, target_partner): + """ If the slide viewed is a certification, we initialize the first survey.user_input + for the current partner. """ + new_slide_partners = super(Slide, self)._action_set_viewed(target_partner) + certification_slides = self.search([ + ('id', 'in', new_slide_partners.mapped('slide_id').ids), + ('slide_type', '=', 'certification'), + ('survey_id', '!=', False) + ]) + + for new_slide_partner in new_slide_partners: + if new_slide_partner.slide_id in certification_slides and not new_slide_partner.user_input_ids: + new_slide_partner.slide_id.survey_id._create_answer( + partner=target_partner, + check_attempts=False, + **{ + 'slide_id': new_slide_partner.slide_id.id, + 'slide_partner_id': new_slide_partner.id + } + ) diff --git a/addons/website_slides_survey/models/survey_survey.py b/addons/website_slides_survey/models/survey_survey.py new file mode 100644 index 00000000000..dc8c91e1599 --- /dev/null +++ b/addons/website_slides_survey/models/survey_survey.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Survey(models.Model): + _inherit = 'survey.survey' + + def _check_answer_creation(self, user, partner, email, test_entry=False, check_attempts=True, invite_token=False): + """ Overridden to allow website_publisher to test certifications. """ + self.ensure_one() + if test_entry and user.has_group('website.group_website_publisher'): + return True + + return super(Survey, self)._check_answer_creation(user, partner, email, test_entry=test_entry, check_attempts=check_attempts, invite_token=invite_token) diff --git a/addons/website_slides_survey/models/survey_user.py b/addons/website_slides_survey/models/survey_user.py new file mode 100644 index 00000000000..427e5196fcb --- /dev/null +++ b/addons/website_slides_survey/models/survey_user.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class SurveyUserInput(models.Model): + _inherit = 'survey.user_input' + + slide_id = fields.Many2one('slide.slide', 'Related course slide', + help="The related course slide when there is no membership information") + slide_partner_id = fields.Many2one('slide.slide.partner', 'Subscriber information', + help="Slide membership information for the logged in user") diff --git a/addons/website_slides_survey/security/ir.model.access.csv b/addons/website_slides_survey/security/ir.model.access.csv new file mode 100644 index 00000000000..15efcd8f67b --- /dev/null +++ b/addons/website_slides_survey/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_survey_publisher,survey.survey.publisher,model_survey_survey,website.group_website_publisher,1,0,0,0 diff --git a/addons/website_slides_survey/static/src/js/slides_certification_download.js b/addons/website_slides_survey/static/src/js/slides_certification_download.js new file mode 100644 index 00000000000..d1469ab63a8 --- /dev/null +++ b/addons/website_slides_survey/static/src/js/slides_certification_download.js @@ -0,0 +1,23 @@ +odoo.define('website_slides_survey.certification_download', function (require) { +'use strict'; + +var WebsiteSlides = require('website_slides.slides'); +var session = require('web.session'); + +WebsiteSlides.include({ + events: _.extend({}, WebsiteSlides.prototype.events, { + 'click .o_wslides_survey_download_certification': '_onDownloadCertificationClick' + }), + + /** + * @private + */ + _onDownloadCertificationClick: function () { + var surveyId = this.$('.o_wslides_survey_download_certification').data('survey_id'); + session.get_file({ + url: '/survey/' + surveyId + '/get_certification' + }); + } +}); + +}); diff --git a/addons/website_slides_survey/static/src/js/slides_upload.js b/addons/website_slides_survey/static/src/js/slides_upload.js new file mode 100644 index 00000000000..817f80004c0 --- /dev/null +++ b/addons/website_slides_survey/static/src/js/slides_upload.js @@ -0,0 +1,96 @@ +odoo.define('website_slides_survey.upload_modal', function (require) { +"use strict"; + +var core = require('web.core'); +var _t = core._t; +var SlidesUpload = require('website_slides.upload_modal'); + +/** + * Management of the new 'certification' slide_type + */ +SlidesUpload.SlideUploadDialog.include({ + /** + * Overridden to add the "certification" slide type + * + * @override + * @private + */ + _setup: function () { + this._super.apply(this, arguments); + this.slide_type_data['certification'] = { + icon: 'fa-trophy', + label: _t('Certification'), + template: 'website.slide.upload.modal.certification', + }; + }, + /** + * Overridden to add certifications management in select2 + * + * @override + * @private + */ + _bindSelect2Dropdown: function () { + this._super.apply(this, arguments); + + var self = this; + this.$('#certification_id').select2(this._select2Wrapper(_t('Certification'), false, + function () { + return self._rpc({ + route: '/slides_survey/certification/search_read', + params: { + fields: ['title'], + } + }); + }, 'title') + ); + }, + /** + * The select2 field makes the "required" input hidden on the interface. + * We need to make the "certification" field required so we override this method + * to handle validation in a fully custom way. + * + * @override + * @private + */ + _formValidate: function () { + var result = this._super.apply(this, arguments); + + var $certificationInput = this.$('#certification_id'); + if ($certificationInput.length !== 0){ + var $select2Container = $certificationInput + .closest('.form-group') + .find('.select2-container'); + $select2Container.removeClass('is-invalid is-valid'); + if ($certificationInput.is(':invalid')) { + $select2Container.addClass('is-invalid'); + } else if ($certificationInput.is(':valid')) { + $select2Container.addClass('is-valid'); + } + } + + return result; + }, + /** + * Overridden to add the 'certification' field into the submitted values + * + * @override + * @private + */ + _getSelect2DropdownValues: function (){ + var result = this._super.apply(this, arguments); + + var certificateValue = this.$('#certification_id').select2('data'); + if (certificateValue) { + result['survey_id'] = certificateValue.id; + } + return result; + } +}); + +SlidesUpload.websiteSlidesUpload.include({ + xmlDependencies: (SlidesUpload.websiteSlidesUpload.prototype.xmlDependencies || []).concat( + ["/website_slides_survey/static/src/xml/website_slide_upload.xml"] + ), +}); + +}); diff --git a/addons/website_slides_survey/static/src/xml/website_slide_upload.xml b/addons/website_slides_survey/static/src/xml/website_slide_upload.xml new file mode 100644 index 00000000000..0b273a15b21 --- /dev/null +++ b/addons/website_slides_survey/static/src/xml/website_slide_upload.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<template> + <t t-name="website.slide.upload.modal.certification"> + <div> + <form class="clearfix"> + <div class="form-group row"> + <label for="certification_id" class="col-form-label col-md-3">Certification</label> + <div class="controls col-md-9"> + <input class="form-control" id="certification_id" required="required"/> + </div> + </div> + <t t-call="website.slide.upload.modal.common"/> + <div class="form-group row"> + <div class="col-md-4"> + <div class="img-thumbnail"> + <div class="o_slide_preview"> + <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 class="col-md-8"> + <ul class="list-group"> + <li class="list-group-item"> + <h5 class="list-group-item-heading"> + <label for="upload" class="col-form-label">Cover Image File</label> + </h5> + <input id="upload" name="file" class="form-control" accept="image/*" type="file" required="required" data-prevent-onchange="1"/> + </li> + </ul> + </div> + </div> + <canvas id="data_canvas" class="d-none"></canvas> + </form> + </div> + </t> +</template> diff --git a/addons/website_slides_survey/views/assets.xml b/addons/website_slides_survey/views/assets.xml new file mode 100644 index 00000000000..8dc06209ad2 --- /dev/null +++ b/addons/website_slides_survey/views/assets.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" ?> +<odoo> + <data> + <template id="assets_frontend" inherit_id="website.assets_frontend" name="Slides Certification"> + <xpath expr="//script[last()]" position="after"> + <script type="text/javascript" src="/website_slides_survey/static/src/js/slides_upload.js"/> + <script type="text/javascript" src="/website_slides_survey/static/src/js/slides_certification_download.js"/> + </xpath> + </template> + + </data> +</odoo> diff --git a/addons/website_slides_survey/views/slide_channel_views.xml b/addons/website_slides_survey/views/slide_channel_views.xml new file mode 100644 index 00000000000..566bef457fa --- /dev/null +++ b/addons/website_slides_survey/views/slide_channel_views.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="slide_channel_view_form" model="ir.ui.view"> + <field name="name">slide.channel.view.form.inherit.survey</field> + <field name="model">slide.channel</field> + <field name="inherit_id" ref="website_slides.view_slide_channel_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='total_slides']" position="before"> + <field name="nbr_certifications"/> + </xpath> + </field> + </record> +</odoo> diff --git a/addons/website_slides_survey/views/slide_slide_views.xml b/addons/website_slides_survey/views/slide_slide_views.xml new file mode 100644 index 00000000000..66083f7bd7f --- /dev/null +++ b/addons/website_slides_survey/views/slide_slide_views.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="slide_slide_view_form" model="ir.ui.view"> + <field name="name">slide.slide.view.form.inherit.survey</field> + <field name="model">slide.slide</field> + <field name="inherit_id" ref="website_slides.view_slide_slide_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='slide_type']" position="after"> + <field name="survey_id" + attrs="{'invisible': [('slide_type', '!=', 'certification')], 'required': [('slide_type', '=', 'certification')]}" + domain="[('certificate', '=', True)]"/> + </xpath> + </field> + </record> +</odoo> diff --git a/addons/website_slides_survey/views/survey_templates.xml b/addons/website_slides_survey/views/survey_templates.xml new file mode 100644 index 00000000000..c6816dde39b --- /dev/null +++ b/addons/website_slides_survey/views/survey_templates.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <template id="survey_templates_inherit_website_slides" inherit_id="survey.sfinished"> + <xpath expr="//div[hasclass('jumbotron')]/div[last()]" position="after"> + <div t-if="slide_url" class="mt16"> + <a role="button" + class="btn btn-primary btn-lg" + t-att-href="slide_url"> + Go back to course + </a> + </div> + </xpath> + </template> + </data> +</odoo> diff --git a/addons/website_slides_survey/views/website_slides_templates.xml b/addons/website_slides_survey/views/website_slides_templates.xml new file mode 100644 index 00000000000..84d9b348fec --- /dev/null +++ b/addons/website_slides_survey/views/website_slides_templates.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <template id="slide_detail_view_inherit_survey" inherit_id="website_slides.slide_detail_view"> + <xpath expr="//div[hasclass('o_w_slides_slide_detail_container')]/div[hasclass('row')]" position="before"> + <t t-if="not test_entry" t-set="begin_certification_label" t-value="'Begin certification'" /> + <t t-if="test_entry" t-set="begin_certification_label" t-value="'Test certification'" /> + <div t-if="slide.slide_type == 'certification' and not quizz_passed" class="mb8"> + <a role="button" + class="btn btn-primary btn-lg" + t-att-href="certification_url"> + <i class="fa fa-fw fa-graduation-cap" role="img" t-att-aria-label="begin_certification_label" t-att-title="begin_certification_label"/> + <t t-esc="begin_certification_label" /> + </a> + </div> + <div t-if="slide.slide_type == 'certification' and quizz_passed" class="mb8"> + <a role="button" + t-att-data-survey_id="survey_id" + class="btn btn-primary btn-lg o_wslides_survey_download_certification" + href="#"> + <i class="fa fa-fw fa-trophy" role="img" aria-label="Download certification" title="Download certification"/> + Download certification + </a> + </div> + </xpath> + </template> + </data> +</odoo> diff --git a/addons/website_slides_survey/views/website_slides_templates_homepage.xml b/addons/website_slides_survey/views/website_slides_templates_homepage.xml new file mode 100644 index 00000000000..24132371f7f --- /dev/null +++ b/addons/website_slides_survey/views/website_slides_templates_homepage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <template id="courses_home_inherit_survey" inherit_id="website_slides.courses_home"> + <xpath expr="//li[hasclass('o_wslides_home_all_slides')]" position="after"> + <li class="nav-item ml16"> + <h4><a class="nav-link" href="/slides/all?slide_type=certification"><i class="fa fa-trophy"/> Certifications</a></h4> + </li> + </xpath> + </template> + </data> +</odoo> |
