Skip to content
Permalink
Browse files

[MERGE][ADD] website_slides_survey: add a new bridge between slide an…

…d 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.
  (A new download route has been added in the survey module to make it work.)
- 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).

closes #31060
  • Loading branch information...
robodoo committed Feb 15, 2019
2 parents 98052fb + 171c702 commit b2149bc7a274217a0b96e587abd52093bd98e5e8
Showing with 807 additions and 59 deletions.
  1. +53 −8 addons/survey/controllers/main.py
  2. +5 −0 addons/survey/models/survey_user.py
  3. +1 −1 addons/survey/views/survey_reports.xml
  4. +22 −10 addons/website_slides/controllers/main.py
  5. +2 −2 addons/website_slides/data/gamification_data.xml
  6. +39 −14 addons/website_slides/models/slide_channel.py
  7. +15 −0 addons/website_slides/models/slide_slide.py
  8. +4 −0 addons/website_slides/static/src/js/slides.js
  9. +29 −11 addons/website_slides/static/src/js/slides_upload.js
  10. +7 −6 addons/website_slides/views/slide_slide_views.xml
  11. +3 −3 addons/website_slides/views/website_slides_templates.xml
  12. +1 −4 addons/website_slides/views/website_slides_templates_homepage.xml
  13. +4 −0 addons/website_slides_survey/__init__.py
  14. +26 −0 addons/website_slides_survey/__manifest__.py
  15. +5 −0 addons/website_slides_survey/controllers/__init__.py
  16. +13 −0 addons/website_slides_survey/controllers/main.py
  17. +49 −0 addons/website_slides_survey/controllers/slides.py
  18. +22 −0 addons/website_slides_survey/controllers/survey.py
  19. +12 −0 addons/website_slides_survey/data/gamification_data.xml
  20. +13 −0 addons/website_slides_survey/data/slide_slide_demo.xml
  21. +108 −0 addons/website_slides_survey/data/survey_demo.xml
  22. +6 −0 addons/website_slides_survey/models/__init__.py
  23. +32 −0 addons/website_slides_survey/models/slide_channel.py
  24. +54 −0 addons/website_slides_survey/models/slide_slide.py
  25. +16 −0 addons/website_slides_survey/models/survey_survey.py
  26. +13 −0 addons/website_slides_survey/models/survey_user.py
  27. +2 −0 addons/website_slides_survey/security/ir.model.access.csv
  28. +23 −0 addons/website_slides_survey/static/src/js/slides_certification_download.js
  29. +96 −0 addons/website_slides_survey/static/src/js/slides_upload.js
  30. +36 −0 addons/website_slides_survey/static/src/xml/website_slide_upload.xml
  31. +12 −0 addons/website_slides_survey/views/assets.xml
  32. +13 −0 addons/website_slides_survey/views/slide_channel_views.xml
  33. +15 −0 addons/website_slides_survey/views/slide_slide_views.xml
  34. +16 −0 addons/website_slides_survey/views/survey_templates.xml
  35. +28 −0 addons/website_slides_survey/views/website_slides_templates.xml
  36. +12 −0 addons/website_slides_survey/views/website_slides_templates_homepage.xml
@@ -12,7 +12,7 @@
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 @@ def _redirect_with_error(self, access_data, error_key):
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 @@ def survey_retry(self, survey_token, answer_token, **post):
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 @@ def survey_display_page(self, survey_token, answer_token, prev=None, **post):
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 @@ def survey_report(self, survey, answer_token=None, **post):
# filter_finish: boolean => only finished surveys or not
#

@http.route(['/survey/<int:survey_id>/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 @@ def _get_scoring_data(self, survey):
'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
@@ -145,6 +145,11 @@ def _send_certification(self):
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'
@@ -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)"
/>
</data>
</odoo>
@@ -75,7 +75,7 @@ def _extract_channel_tag_search(self, **post):
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 @@ def _build_channel_domain(self, base_domain, **post):
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 @@ def slides_channel_home(self, **post):

# 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 @@ def slides_channel_home(self, **post):
})

@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 @@ def channel(self, channel, category=None, tag=None, page=1, slide_type=None, sor
'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 @@ def slide_view(self, slide, **kwargs):
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 @@ def create_slide(self, *args, **post):
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 @@ def create_slide(self, *args, **post):
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)
@@ -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">
@@ -157,29 +157,39 @@ def _compute_is_member(self):
@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'],
groupby=['channel_id', 'slide_type'],
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 @@ def _count_presentations(self):
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
@@ -126,6 +126,8 @@ def _default_access_token(self):
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 @@ def _compute_slide_views(self):
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] == '/':
@@ -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'));
},
});

});
Oops, something went wrong.

0 comments on commit b2149bc

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.