diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 488250ce4a1..446caf6a55e 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -12,7 +12,7 @@ from math import ceil from odoo import fields, http, _ from odoo.addons.base.models.ir_ui_view import keep_query from odoo.exceptions import UserError -from odoo.http import request +from odoo.http import request, content_disposition from odoo.tools import ustr _logger = logging.getLogger(__name__) @@ -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/', 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) @@ -501,6 +509,37 @@ class Survey(http.Controller): # filter_finish: boolean => only finished surveys or not # + @http.route(['/survey//get_certification'], type='http', auth='user', methods=['POST'], website=True) + def survey_get_certification(self, survey_id, **kwargs): + """ The certification document can be downloaded as long as the user has succeeded the certification """ + survey = request.env['survey.survey'].sudo().search([ + ('id', '=', survey_id), + ('certificate', '=', True) + ]) + + if not survey: + # no certification found + return werkzeug.utils.redirect("/") + + succeeded_attempt = request.env['survey.user_input'].sudo().search([ + ('partner_id', '=', request.env.user.partner_id.id), + ('survey_id', '=', survey_id), + ('quizz_passed', '=', True) + ], limit=1) + + if not succeeded_attempt: + raise UserError(_("The user has not succeeded the certification")) + + report_sudo = request.env.ref('survey.certification_report').sudo() + + report = report_sudo.render_qweb_pdf([succeeded_attempt.id], data={'report_type': 'pdf'})[0] + reporthttpheaders = [ + ('Content-Type', 'application/pdf'), + ('Content-Length', len(report)), + ] + reporthttpheaders.append(('Content-Disposition', content_disposition('Certification.pdf'))) + return request.make_response(report, headers=reporthttpheaders) + def _prepare_result_dict(self, survey, current_filters=None): """Returns dictionary having values for rendering template""" current_filters = current_filters if current_filters else [] @@ -596,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/survey/views/survey_reports.xml b/addons/survey/views/survey_reports.xml index fb8783b5136..28a41c6bffe 100644 --- a/addons/survey/views/survey_reports.xml +++ b/addons/survey/views/survey_reports.xml @@ -10,7 +10,7 @@ name="survey.certification_report_view" file="survey.certification_report_view" attachment="'certification.pdf'" - print_report_name="'Certification - %s' % (object.survey_id.name)" + print_report_name="'Certification - %s' % (object.survey_id.display_name)" /> diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 4b0db549ef6..6a75730daa0 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -75,7 +75,7 @@ class WebsiteSlides(WebsiteProfile): tags |= search_tag return tags - def _build_channel_domain(self, base_domain, **post): + def _build_channel_domain(self, base_domain, slide_type=None, **post): search_term = post.get('search') category_id = post.get('category_id') channel_tag_id = post.get('channel_tag_id') @@ -92,6 +92,9 @@ class WebsiteSlides(WebsiteProfile): domain = expression.AND([domain, [('tag_ids', 'in', [channel_tag_id])]]) elif tags: domain = expression.AND([domain, [('tag_ids', 'in', tags.ids)]]) + + if slide_type and 'nbr_%ss' % slide_type in request.env['slide.channel']: + domain = expression.AND([domain, [('nbr_%ss' % slide_type, '>', 0)]]) return domain # -------------------------------------------------- @@ -112,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 @@ -144,17 +151,19 @@ class WebsiteSlides(WebsiteProfile): }) @http.route('/slides/all', type='http', auth="public", website=True) - def slides_channel_all(self, **post): + def slides_channel_all(self, slide_type=None, **post): """ Home page displaying a list of courses displayed according to some criterion and search terms. + : param string slide_type: if provided, filter the slide.channels + to contain at least one slide of type 'slide_type' : param dict post: post parameters, including * search_term: keywords entered in the search box, used to filter on slide content; * category_id: id of a slide.category; * channel_tag_id: id of a channel.tag; """ domain = request.website.website_domain() - domain = self._build_channel_domain(domain, **post) + domain = self._build_channel_domain(domain, slide_type=slide_type, **post) order = self._channel_order_by_criterion.get(post.get('sorting', 'date'), 'create_date desc') @@ -244,6 +253,7 @@ class WebsiteSlides(WebsiteProfile): 'user': request.env.user, 'pager': pager, 'is_public_user': request.website.is_public_user(), + 'is_slides_publisher': request.env.user.has_group('website.group_website_publisher'), } if not request.env.user._is_public(): last_message_values = request.env['mail.message'].search([ @@ -319,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({ @@ -416,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: @@ -459,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 @@ Get a certification count boolean - + higher [ ('completed', '=', True), (0, '=', 1) ] True - + user.partner_id.id diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index c94cda59b8c..9d51ea52d1c 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((cid, dict(total_slides=0, total_views=0, total_votes=0, total_time=0)) for cid in self.ids) 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((cid, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0)) for cid in self.ids) + 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 93eb04e9bb5..7e7fcdf92e4 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -47,9 +47,7 @@
@@ -73,10 +71,13 @@ - + - - + + diff --git a/addons/website_slides/views/website_slides_templates.xml b/addons/website_slides/views/website_slides_templates.xml index 6bbd8d91292..af82d23701c 100644 --- a/addons/website_slides/views/website_slides_templates.xml +++ b/addons/website_slides/views/website_slides_templates.xml @@ -273,7 +273,7 @@

- + @@ -324,7 +324,7 @@ t-att-alt="channel.name"/>
- + @@ -392,7 +392,7 @@
-
+
Slide image 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,12 +22,9 @@