[ADD] website_event_track_quiz: add quiz feature on event.track

RATIONALE

Events are sometimes held online, gathering a community. In this merge we
improve Event application to better support full-online events with improved
tracks, wishlists, chat rooms, ...

PURPOSE

Allow to configure a quiz on event tracks, much like eLearning.
The quiz is taken at the end of the live stream / track video.
(Or when the talk is 'live' of 'done').

SPECIFICATIONS

Users can now configure an event.quiz on event.track.

The quiz is a simple collection of event.quiz.questions that in turn contain
a few event.quiz.answers.
Only one of the answer is considered as correct and should be configured with
"awarded_points" greater than 0.

On the event.track frontend page, attendees can take the quiz when the track
is "live" (or "done") and earn the total amount of configured awarded_points.
Each quiz can only be taken once (if you fail, you can't try again).

Attendees are then ranked in a leaderboard that is displayed in the "Community"
section of the event frontend menus.
The leaderboard allows to see your own position as well as basic search for
other attendees.
The points earned during the event are event-specific and will not be shared
with other instances of event.event.

Points as well as quiz completion are stored in the relationship between a
visitor and a track (event.track.visitor).

The models are very much based on a similar feature in website_slides, even
though the implementation in event is not karma based.
We will probably have to merge the features later into a base gamification_quiz
module.

LINKS

Task ID-2252655 (Main Online Event task)
Task ID-2283771 (Quizzes on Talks, Leaderboard)

Co-Authored-By: Aurélien Warnon <awa@odoo.com>
Co-Authored-By: David Beguin <dbe@odoo.com>
Co-Authored-By: Quentin Mourier <qmo@odoo.com>
Co-Authored-By: Thibault Delavallée <tde@odoo.com>
This commit is contained in:
qmo-odoo
2020-08-04 14:28:29 +00:00
committed by Thibault Delavallée
co-authored by Aurélien Warnon David Beguin Thibault Delavallée
parent fab82c73a5
commit fe17857e89
25 changed files with 1463 additions and 27 deletions
-23
View File
@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Gamification Quizzes',
'category': 'Marketing/Events',
'sequence': 1000,
'version': '1.0',
'summary': 'Gamification quizzes and leaderboard',
'website': 'https://www.odoo.com/page/events',
'description': "",
'depends': [
'gamification',
'website_profile',
],
'data': [
],
'demo': [
],
'application': False,
'installable': True,
}
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
@@ -11,13 +11,24 @@
'website': 'https://www.odoo.com/page/events',
'description': "",
'depends': [
'gamification_quiz',
'website_event_track_online',
'website_profile',
'website_event_track_session',
],
'data': [
'security/ir.model.access.csv',
'views/assets.xml',
'views/event_leaderboard_templates.xml',
'views/event_quiz_views.xml',
'views/event_quiz_question_views.xml',
'views/event_track_views.xml',
'views/event_menus.xml',
'views/event_quiz_templates.xml',
'views/event_track_templates_page.xml',
'views/event_type_views.xml'
],
'demo': [
'data/quiz_demo.xml',
],
'application': True,
'installable': True, # set to False
'application': False,
'installable': True,
}
@@ -1,2 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_track_quiz
from . import community
@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import math
from odoo import http
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.website_event_track_online.controllers.community import WebsiteEventCommunityController
from odoo.http import request
class WebsiteEventTrackQuizCommunityController(WebsiteEventCommunityController):
_visitors_per_page = 30
_pager_max_pages = 5
@http.route(['/event/<model("event.event"):event>/community/leaderboard/results',
'/event/<model("event.event"):event>/community/leaderboard/results/page/<int:page>'],
type='http', auth="public", website=True, sitemap=False)
def leaderboard(self, event, page=1, lang=None, **kwargs):
values = self._get_community_leaderboard_render_values(event, kwargs.get('search'), page)
return request.render('website_event_track_quiz.event_leaderboard', values)
@http.route('/event/<model("event.event"):event>/community/leaderboard',
type='http', auth="public", website=True, sitemap=False)
def community_leaderboard(self, event, **kwargs):
values = self._get_community_leaderboard_render_values(event, None, None)
return request.render('website_event_track_quiz.event_leaderboard', values)
@http.route('/event/<model("event.event"):event>/community',
type='http', auth="public", website=True, sitemap=False)
def community(self, event, **kwargs):
values = self._get_community_leaderboard_render_values(event, None, None)
return request.render('website_event_track_quiz.event_leaderboard', values)
def _get_community_leaderboard_render_values(self, event, search_term, page):
values = self._get_leaderboard(event, search_term)
values.update({'event': event, 'search': search_term})
user_count = len(values['visitors'])
if user_count:
page_count = math.ceil(user_count / self._visitors_per_page)
url = '/event/%s/community/leaderboard/results' % (slug(event))
if values.get('current_visitor_position') and not page:
values['scroll_to_position'] = True
page = math.ceil(values['current_visitor_position'] / self._visitors_per_page)
elif not page:
page = 1
pager = request.website.pager(url=url, total=user_count, page=page, step=self._visitors_per_page,
scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages)
values['visitors'] = values['visitors'][(page - 1) * self._visitors_per_page: (page) * self._visitors_per_page]
else:
pager = {'page_count': 0}
values.update({'pager': pager})
return values
def _get_leaderboard(self, event, searched_name=None):
current_visitor = request.env['website.visitor']._get_visitor_from_request(force_create=False)
track_visitor_data = request.env['event.track.visitor'].sudo().read_group(
[('track_id', 'in', event.track_ids.ids),
('visitor_id', '!=', False),
('quiz_points', '>', 0)],
['id', 'visitor_id', 'points:sum(quiz_points)'],
['visitor_id'], orderby="points DESC")
data_map = {datum['visitor_id'][0]: datum['points'] for datum in track_visitor_data if datum.get('visitor_id')}
leaderboard = []
position = 1
current_visitor_position = False
visitors_by_id = {
visitor.id: visitor
for visitor in request.env['website.visitor'].sudo().browse(data_map.keys())
}
for visitor_id, points in data_map.items():
visitor = visitors_by_id.get(visitor_id)
if not visitor:
continue
if (searched_name and searched_name.lower() in visitor.display_name.lower()) or not searched_name:
leaderboard.append({'visitor': visitor, 'points': points, 'position': position})
if current_visitor and current_visitor == visitor:
current_visitor_position = position
position = position + 1
return {
'top3_visitors': leaderboard[:3] if len(leaderboard) >= 3 else False,
'visitors': leaderboard,
'current_visitor_position': current_visitor_position,
'current_visitor': current_visitor,
'searched_name': searched_name
}
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.addons.website_event_track_session.controllers.session import WebsiteEventSessionController
from odoo.http import request
class WebsiteEventTrackQuiz(WebsiteEventSessionController):
# QUIZZES IN PAGE
# ----------------------------------------------------------
@http.route('/event_track/quiz/submit', type="json", auth="public", website=True)
def event_track_quiz_submit(self, event_id, track_id, answer_ids):
track = self._fetch_track(track_id)
event_track_visitor = track._get_event_track_visitors(force_create=True)
visitor_sudo = event_track_visitor.visitor_id
if event_track_visitor.quiz_completed:
return {'error': 'track_quiz_done'}
answers_details = self._get_quiz_answers_details(track, answer_ids)
if answers_details.get('error'):
return answers_details
event_track_visitor.write({
'quiz_completed': True,
'quiz_points': answers_details['points'],
})
result = {
'answers': {
answer.question_id.id: {
'is_correct': answer.is_correct,
'comment': answer.comment
} for answer in answers_details['user_answers']
},
'quiz_completed': event_track_visitor.quiz_completed,
'quiz_points': answers_details['points']
}
if visitor_sudo and request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
result['visitor_uuid'] = visitor_sudo.access_token
return result
@http.route('/event_track/quiz/reset', type="json", auth="user", website=True)
def quiz_reset(self, event_id, track_id):
track = self._fetch_track(track_id)
event_track_visitor = track._get_event_track_visitors(force_create=True)
event_track_visitor.write({
'quiz_completed': False,
'quiz_points': 0,
})
def _get_quiz_answers_details(self, track, answer_ids):
# TDE FIXME: lost sudo
questions_count = request.env['event.quiz.question'].sudo().search_count([('quiz_id', '=', track.sudo().quiz_id.id)])
user_answers = request.env['event.quiz.answer'].sudo().search([('id', 'in', answer_ids)])
if len(user_answers.mapped('question_id')) != questions_count:
return {'error': 'quiz_incomplete'}
return {
'user_answers': user_answers,
'points': sum([
answer.awarded_points
for answer in user_answers.filtered(lambda answer: answer.is_correct)
])
}
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="event_7_track_1_quiz" model="event.quiz">
<field name="name">What This Event Is All About</field>
</record>
<record id="event_7_track_1_question_0" model="event.quiz.question">
<field name="name">What will we talk about during this event ?</field>
<field name="sequence">1</field>
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_1_quiz"/>
</record>
<record id="event_7_track_1_question_0_0" model="event.quiz.answer">
<field name="text_value">Wood</field>
<field name="sequence">1</field>
<field name="awarded_points">1</field>
<field name="comment">Correct ! You're really smart !</field>
<field name="question_id" ref="event_7_track_1_question_0"/>
</record>
<record id="event_7_track_1_question_0_1" model="event.quiz.answer">
<field name="text_value">Music</field>
<field name="sequence">2</field>
<field name="comment">Incorrect ! Even if there will be some.</field>
<field name="question_id" ref="event_7_track_1_question_0"/>
</record>
<record id="event_7_track_1_question_0_2" model="event.quiz.answer">
<field name="text_value">Open Source Apps</field>
<field name="sequence">3</field>
<field name="comment">Incorrect ! OpenWood is not an Open Source congres about Apps.</field>
<field name="question_id" ref="event_7_track_1_question_0"/>
</record>
<record id="event_7_track_1_question_1" model="event.quiz.question">
<field name="name">Where does lumber comes from ?</field>
<field name="sequence">2</field>
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_1_quiz"/>
</record>
<record id="event_7_track_1_question_1_0" model="event.quiz.answer">
<field name="text_value">Trees !</field>
<field name="sequence">1</field>
<field name="awarded_points">1</field>
<field name="comment">Correct !</field>
<field name="question_id" ref="event_7_track_1_question_1"/>
</record>
<record id="event_7_track_1_question_1_1" model="event.quiz.answer">
<field name="text_value">Stores !</field>
<field name="sequence">2</field>
<field name="comment">Incorrect ! Lumbers need first to be cut from trees!</field>
<field name="question_id" ref="event_7_track_1_question_1"/>
</record>
<record id="website_event_track.event_7_track_1" model="event.track">
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_1_quiz"/>
</record>
<record id="event_7_track_5_quiz" model="event.quiz">
<field name="name">Securing your Lumber during transport</field>
</record>
<record id="event_7_track_5_question_0" model="event.quiz.question">
<field name="name">Transporting lumber from stores to your house is safe.</field>
<field name="sequence">1</field>
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_5_quiz"/>
</record>
<record id="event_7_track_5_question_0_0" model="event.quiz.answer">
<field name="text_value">Yes</field>
<field name="sequence">1</field>
<field name="comment">Incorrect! In order to avoid accident, you need to secure any product of this kind during transportation!</field>
<field name="question_id" ref="event_7_track_5_question_0"/>
</record>
<record id="event_7_track_5_question_0_1" model="event.quiz.answer">
<field name="text_value">No</field>
<field name="sequence">2</field>
<field name="awarded_points">1</field>
<field name="comment">Correct! Even if you have a big trunk, some long products need to be secured.</field>
<field name="question_id" ref="event_7_track_5_question_0"/>
</record>
<record id="event_7_track_5_question_1" model="event.quiz.question">
<field name="name">What kind of tool are needed to secure your lumber ?</field>
<field name="sequence">2</field>
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_5_quiz"/>
</record>
<record id="event_7_track_5_question_1_0" model="event.quiz.answer">
<field name="text_value">Hammer</field>
<field name="sequence">1</field>
<field name="comment">Incorrect! Hammer won't be of any help here!</field>
<field name="question_id" ref="event_7_track_5_question_1"/>
</record>
<record id="event_7_track_5_question_1_1" model="event.quiz.answer">
<field name="text_value">Tie-down straps and other wooden blocks</field>
<field name="sequence">2</field>
<field name="awarded_points">1</field>
<field name="comment">Correct!</field>
<field name="question_id" ref="event_7_track_5_question_1"/>
</record>
<record id="event_7_track_5_question_1_2" model="event.quiz.answer">
<field name="text_value">Scotch tape</field>
<field name="sequence">3</field>
<field name="comment">Incorrect! Well, it could work but you will need a lot of tape!</field>
<field name="question_id" ref="event_7_track_5_question_1"/>
</record>
<record id="website_event_track.event_7_track_5" model="event.track">
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_5_quiz"/>
</record>
<record id="event_7_track_13_quiz" model="event.quiz">
<field name="name">Pretty. Ugly. Lovely.</field>
</record>
<record id="event_7_track_13_question_0" model="event.quiz.question">
<field name="name">What kind of wall is transformed here ?</field>
<field name="sequence">1</field>
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_13_quiz"/>
</record>
<record id="event_7_track_13_question_0_0" model="event.quiz.answer">
<field name="text_value">Concrete Blocks Wall</field>
<field name="sequence">1</field>
<field name="awarded_points">1</field>
<field name="comment">Correct!</field>
<field name="question_id" ref="event_7_track_13_question_0"/>
</record>
<record id="event_7_track_13_question_0_1" model="event.quiz.answer">
<field name="text_value">Steel Wall</field>
<field name="sequence">2</field>
<field name="comment">Incorrect!</field>
<field name="question_id" ref="event_7_track_13_question_0"/>
</record>
<record id="event_7_track_13_question_0_2" model="event.quiz.answer">
<field name="text_value">Mud Wall</field>
<field name="sequence">3</field>
<field name="comment">Incorrect!</field>
<field name="question_id" ref="event_7_track_13_question_0"/>
</record>
<record id="website_event_track.event_7_track_13" model="event.track">
<field name="quiz_id" ref="website_event_track_quiz.event_7_track_13_quiz"/>
</record>
<record id="event.event_7" model="event.event">
<field name="community_menu" eval="True"/>
</record>
<record id="event_track_visitor_admin_event_7_track_1" model="event.track.visitor">
<field name="track_id" ref="website_event_track.event_7_track_1"/>
<field name="visitor_id" ref="website_event_online.website_visitor_event_0"/>
<field name="quiz_completed" eval="True"/>
<field name="quiz_points">3</field>
</record>
<record id="event_track_visitor_demo_event_7_track_1" model="event.track.visitor">
<field name="track_id" ref="website_event_track.event_7_track_1"/>
<field name="visitor_id" ref="website_event_online.website_visitor_event_1"/>
<field name="quiz_completed" eval="True"/>
<field name="quiz_points">2</field>
</record>
<record id="event_track_visitor_portal_event_7_track_1" model="event.track.visitor">
<field name="track_id" ref="website_event_track.event_7_track_1"/>
<field name="visitor_id" ref="website_event_online.website_visitor_event_2"/>
<field name="quiz_completed" eval="True"/>
<field name="quiz_points">1</field>
</record>
</data></odoo>
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_track
from . import event_track_visitor
from . import event_quiz
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class Quiz(models.Model):
_name = "event.quiz"
_description = "Quiz"
name = fields.Char('Name', required=True, translate=True)
question_ids = fields.One2many('event.quiz.question', 'quiz_id', string="Questions")
event_track_ids = fields.One2many('event.track', 'quiz_id', string="Tracks")
event_track_id = fields.Many2one(
'event.track', compute='_compute_event_track_id',
readonly=True, store=True)
event_id = fields.Many2one(
'event.event', related='event_track_id.event_id',
readonly=True, store=True)
@api.depends('event_track_ids.quiz_id')
def _compute_event_track_id(self):
for quiz in self:
quiz.event_track_id = quiz.event_track_ids[0] if quiz.event_track_ids else False
class QuizQuestion(models.Model):
_name = "event.quiz.question"
_description = "Content Quiz Question"
_order = "quiz_id, sequence, id"
name = fields.Char("Question", required=True, translate=True)
sequence = fields.Integer("Sequence")
quiz_id = fields.Many2one("event.quiz", "Quiz", required=True, ondelete='cascade')
awarded_points = fields.Integer("Number of Points", compute='_compute_awarded_points')
answer_ids = fields.One2many('event.quiz.answer', 'question_id', string="Answer")
@api.depends('answer_ids.awarded_points')
def _compute_awarded_points(self):
for question in self:
question.awarded_points = sum(question.answer_ids.mapped('awarded_points'))
@api.constrains('answer_ids')
def _check_answers_integrity(self):
for question in self:
if len(question.answer_ids.filtered(lambda answer: answer.awarded_points)) != 1:
raise ValidationError(_('Question "%s" must have 1 correct answer') % question.name)
if len(question.answer_ids) < 2:
raise ValidationError(_('Question "%s" must have 1 correct answer and at least 1 invalid answer') % question.name)
class QuizAnswer(models.Model):
_name = "event.quiz.answer"
_rec_name = "text_value"
_description = "Question's Answer"
_order = 'question_id, sequence, id'
sequence = fields.Integer("Sequence")
question_id = fields.Many2one('event.quiz.question', string="Question", required=True, ondelete='cascade')
text_value = fields.Char("Answer", required=True, translate=True)
is_correct = fields.Boolean("Is correct answer", compute='_compute_is_correct')
comment = fields.Text("Comment", translate=True,
help='''This comment will be displayed to the user if he selects this answer, after submitting the quiz.
It is used as a small informational text helping to understand why this answer is correct / incorrect.''')
awarded_points = fields.Integer('Number of Points')
@api.depends('awarded_points')
def _compute_is_correct(self):
for answer in self:
answer.is_correct = bool(answer.awarded_points)
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.osv import expression
class EventTrack(models.Model):
_inherit = ['event.track']
quiz_id = fields.Many2one('event.quiz', string="Quiz", groups="event.group_event_user")
quiz_questions_count = fields.Integer(string="# Quiz Questions", compute='_compute_quiz_questions_count', groups="event.group_event_user")
is_quiz_completed = fields.Boolean('Is Quiz Done', compute='_compute_quiz_data')
quiz_points = fields.Integer('Quiz Points', compute='_compute_quiz_data')
@api.depends('quiz_id.question_ids')
def _compute_quiz_questions_count(self):
for track in self:
track.quiz_questions_count = len(track.quiz_id.question_ids)
@api.depends('quiz_id', 'event_track_visitor_ids.visitor_id',
'event_track_visitor_ids.partner_id', 'event_track_visitor_ids.quiz_completed',
'event_track_visitor_ids.quiz_points')
@api.depends_context('uid')
def _compute_quiz_data(self):
tracks_quiz = self.filtered(lambda track: track.quiz_id)
(self - tracks_quiz).is_quiz_completed = False
(self - tracks_quiz).quiz_points = 0
if tracks_quiz:
current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
if self.env.user._is_public() and not current_visitor:
for track in tracks_quiz:
track.is_quiz_completed = False
track.quiz_points = 0
else:
if self.env.user._is_public():
domain = [('visitor_id', '=', current_visitor.id)]
elif current_visitor:
domain = [
'|',
('partner_id', '=', self.env.user.partner_id.id),
('visitor_id', '=', current_visitor.id)
]
else:
domain = [('partner_id', '=', self.env.user.partner_id.id)]
event_track_visitors = self.env['event.track.visitor'].sudo().search_read(
expression.AND([
domain,
[('track_id', 'in', tracks_quiz.ids)]
]), fields=['track_id', 'quiz_completed', 'quiz_points']
)
quiz_visitor_map = {
track_visitor['track_id'][0]: {
'quiz_completed': track_visitor['quiz_completed'],
'quiz_points': track_visitor['quiz_points']
} for track_visitor in event_track_visitors
}
for track in tracks_quiz:
if quiz_visitor_map.get(track.id):
track.is_quiz_completed = quiz_visitor_map[track.id]['quiz_completed']
track.quiz_points = quiz_visitor_map[track.id]['quiz_points']
else:
track.is_quiz_completed = False
track.quiz_points = 0
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class TrackVisitor(models.Model):
_name = 'event.track.visitor'
_inherit = ['event.track.visitor']
quiz_completed = fields.Boolean('Completed')
quiz_points = fields.Integer("Quiz Points", default=0)
@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
event_quiz_access_event_user,event.quiz.access.event.user,model_event_quiz,event.group_event_user,1,0,0,0
event_quiz_access_event_manager,event.quiz.access.event.manager,model_event_quiz,event.group_event_manager,1,1,1,1
event_quiz_question_access_event_user,event.quiz.question.access.event.user,model_event_quiz_question,event.group_event_user,1,0,0,0
event_quiz_question_access_event_manager,event.quiz.question.access.event.manager,model_event_quiz_question,event.group_event_manager,1,1,1,1
event_quiz_answer_access_event_user,event.quiz.answer.event.user,model_event_quiz_question_answer,event.group_event_user,1,0,0,0
event_quiz_answer_access_event_manager,event.quiz.answer.event.manager,model_event_quiz_question_answer,event.group_event_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 event_quiz_access_event_user event.quiz.access.event.user model_event_quiz event.group_event_user 1 0 0 0
3 event_quiz_access_event_manager event.quiz.access.event.manager model_event_quiz event.group_event_manager 1 1 1 1
4 event_quiz_question_access_event_user event.quiz.question.access.event.user model_event_quiz_question event.group_event_user 1 0 0 0
5 event_quiz_question_access_event_manager event.quiz.question.access.event.manager model_event_quiz_question event.group_event_manager 1 1 1 1
6 event_quiz_answer_access_event_user event.quiz.answer.event.user model_event_quiz_question_answer event.group_event_user 1 0 0 0
7 event_quiz_answer_access_event_manager event.quiz.answer.event.manager model_event_quiz_question_answer event.group_event_manager 1 1 1 1
@@ -0,0 +1,323 @@
odoo.define('website_event_track_quiz.event.quiz', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
var core = require('web.core');
var session = require('web.session');
var utils = require('web.utils');
var QWeb = core.qweb;
var _t = core._t;
/**
* This widget is responsible of displaying quiz questions and propositions. Submitting the quiz will fetch the
* correction and decorate the answers according to the result. Error message can be displayed.
*
* This widget can be attached to DOM rendered server-side by `gamification_quiz.`
*
*/
var Quiz = publicWidget.Widget.extend({
template: 'quiz.main',
xmlDependencies: ['/website_event_track_quiz/static/src/xml/quiz_templates.xml'],
events: {
"click .o_quiz_quiz_answer": '_onAnswerClick',
"click .o_quiz_js_quiz_submit": '_submitQuiz',
"click .o_quiz_js_quiz_reset": '_onClickReset',
},
/**
* @override
* @param {Object} parent
* @param {Object} data holding all the container information
* @param {Object} quizData : quiz data to display
*/
init: function (parent, data, quizData) {
this._super.apply(this, arguments);
this.track = _.defaults(data, {
id: 0,
name: '',
eventId: '',
completed: false,
isMember: false,
progressBar: false,
isManager: false
});
this.quiz = quizData || false;
if (this.quiz) {
this.quiz.questionsCount = quizData.questions.length;
}
this.isMember = data.isMember || false;
this.userId = session.user_id;
this.redirectURL = encodeURIComponent(document.URL);
},
/**
* @override
*/
willStart: function () {
var defs = [this._super.apply(this, arguments)];
if (!this.quiz) {
defs.push(this._fetchQuiz());
}
return Promise.all(defs);
},
/**
* Overridden to add custom rendering behavior upon start of the widget.
*
* If the user has answered the quiz before having joined the course, we check
* his answers (saved into his session) here as well.
*
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self._renderValidationInfo();
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_alertShow: function (alertCode) {
var message = _t('There was an error validating this quiz.');
if (alertCode === 'quiz_incomplete') {
message = _t('All questions must be answered !');
} else if (alertCode === 'quiz_done') {
message = _t('This quiz is already done. Retaking it is not possible.');
}
this.displayNotification({
type: 'warning',
title: _t('Quiz validation error'),
message: message,
sticky: true
});
},
/**
* Get all the questions ID from the displayed Quiz
* @returns {Array}
* @private
*/
_getQuestionsIds: function () {
return this.$('.o_quiz_js_quiz_question').map(function () {
return $(this).data('question-id');
}).get();
},
/**
* @private
* Decorate the answers according to state
*/
_disableAnswers: function () {
var self = this;
this.$('.o_quiz_js_quiz_question').addClass('completed-disabled');
this.$('input[type=radio]').each(function () {
$(this).prop('disabled', self.track.completed);
});
},
/**
* Decorate the answer inputs according to the correction and adds the answer comment if
* any.
*
* @private
*/
_renderAnswersHighlightingAndComments: function () {
var self = this;
this.$('.o_quiz_js_quiz_question').each(function () {
var $question = $(this);
var questionId = $question.data('questionId');
var isCorrect = self.quiz.answers[questionId].is_correct;
$question.find('a.o_quiz_quiz_answer').each(function () {
var $answer = $(this);
$answer.find('i.fa').addClass('d-none');
if ($answer.find('input[type=radio]')[0].checked) {
if (isCorrect) {
$answer.removeClass('list-group-item-danger').addClass('list-group-item-success');
$answer.find('i.fa-check-circle').removeClass('d-none');
} else {
$answer.removeClass('list-group-item-success').addClass('list-group-item-danger');
$answer.find('i.fa-times-circle').removeClass('d-none');
$answer.find('label input').prop('checked', false);
}
} else {
$answer.removeClass('list-group-item-danger list-group-item-success');
$answer.find('i.fa-circle').removeClass('d-none');
}
});
var comment = self.quiz.answers[questionId].comment;
if (comment) {
$question.find('.o_quiz_quiz_answer_info').removeClass('d-none');
$question.find('.o_quiz_quiz_answer_comment').text(comment);
}
});
},
/*
* @private
* Update validation box (karma, buttons) according to widget state
*/
_renderValidationInfo: function () {
var $validationElem = this.$('.o_quiz_js_quiz_validation');
$validationElem.html(
QWeb.render('quiz.validation', {'widget': this})
);
},
/**
* Get the quiz answers filled in by the User
*
* @private
*/
_getQuizAnswers: function () {
return this.$('input[type=radio]:checked').map(function (index, element) {
return parseInt($(element).val());
}).get();
},
/**
* Submit a quiz and get the correction. It will display messages
* according to quiz result.
*
* @private
*/
_submitQuiz: function () {
var self = this;
return this._rpc({
route: '/event_track/quiz/submit',
params: {
event_id: self.track.eventId,
track_id: self.track.id,
answer_ids: this._getQuizAnswers(),
}
}).then(function (data) {
if (data.error) {
self._alertShow(data.error);
} else {
self.quiz = _.extend(self.quiz, data);
self.quiz.quizPointsGained = data.quiz_points;
if (data.quiz_completed) {
self._disableAnswers();
self.track.completed = data.quiz_completed;
}
self._renderAnswersHighlightingAndComments();
self._renderValidationInfo();
if (data.visitor_uuid) {
utils.set_cookie('visitor_uuid', data.visitor_uuid);
}
}
return Promise.resolve(data);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* When clicking on an answer, this one should be marked as "checked".
*
* @private
* @param OdooEvent ev
*/
_onAnswerClick: function (ev) {
ev.preventDefault();
if (!this.track.completed) {
$(ev.currentTarget).find('input[type=radio]').prop('checked', true);
}
},
/**
* Resets the completion of the track so the user can take
* the quiz again
*
* @private
*/
_onClickReset: function () {
this._rpc({
route: '/event_track/quiz/reset',
params: {
event_id: this.track.eventId,
track_id: this.track.id
}
}).then(function () {
window.location.reload();
});
},
});
publicWidget.registry.Quiz = publicWidget.Widget.extend({
selector: '.o_quiz_main',
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* @override
* @param {Object} parent
*/
start: function () {
var self = this;
this.quizWidgets = [];
var defs = [this._super.apply(this, arguments)];
this.$('.o_quiz_js_quiz').each(function () {
var data = $(this).data();
data.quizData = {
questions: self._extractQuestionsAndAnswers(),
sessionAnswers: data.sessionAnswers || [],
quizKarmaMax: data.quizKarmaMax,
quizKarmaWon: data.quizKarmaWon,
quizKarmaGain: data.quizKarmaGain,
quizPointsGained: data.quizPointsGained,
quizAttemptsCount: data.quizAttemptsCount,
};
defs.push(new Quiz(self, data, data.quizData).attachTo($(this)));
});
return Promise.all(defs);
},
//----------------------------------------------------------------------
// Private
//---------------------------------------------------------------------
/**
* Extract data from exiting DOM rendered server-side, to have the list of questions with their
* relative answers.
* This method should return the same format as /gamification_quiz/quiz/get controller.
*
* @return {Array<Object>} list of questions with answers
*/
_extractQuestionsAndAnswers: function () {
var questions = [];
this.$('.o_quiz_js_quiz_question').each(function () {
var $question = $(this);
var answers = [];
$question.find('.o_quiz_quiz_answer').each(function () {
var $answer = $(this);
answers.push({
id: $answer.data('answerId'),
text: $answer.data('text'),
});
});
questions.push({
id: $question.data('questionId'),
title: $question.data('title'),
answer_ids: answers,
});
});
return questions;
},
});
return Quiz;
});
@@ -0,0 +1,34 @@
odoo.define('website_event_track_quiz.event_leaderboard', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
publicWidget.registry.EventLeaderboard = publicWidget.Widget.extend({
selector: '.o_wevent_quiz_leaderboard',
/**
* Basic override to scroll to current visitor's position.
*/
start: function () {
var self = this;
return this._super(...arguments).then(function () {
var $scrollTo = self.$('.o_wevent_quiz_scroll_to');
if ($scrollTo.length !== 0) {
var offset = $('.o_header_standard').height();
var $appMenu = $('.o_main_navbar');
if ($appMenu.length !== 0) {
offset += $appMenu.height();
}
window.scrollTo({
top: $scrollTo.offset().top - offset,
behavior: 'smooth'
});
}
});
}
});
return publicWidget.registry.EventLeaderboard;
});
@@ -0,0 +1,92 @@
// Retrive the tab's height by summ its properties
$o-wslides-tabs-height: ($nav-link-padding-y*2) + ($font-size-base * $line-height-base);
// Overal page bg-color: Blend it 'over' the color chosen by the user
// ($body-bg), rather than force it replacing the variable's value.
$o-wslides-color-bg: mix($body-bg, #efeff4);
$o-wslides-color-dark1: #47525f;
$o-wslides-color-dark2: #1f262d;
$o-wslides-color-dark3: #101216;
$o-wslides-fs-side-width: 300px;
// Common to new slides pages
// **************************************************
.o_quiz_gradient {
background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%));
}
.o_quiz_main {
background-color: $o-wslides-color-bg;
.o_wslides_home_nav {
top: -40px;
@include media-breakpoint-up(lg) {
font-size: 1rem;
.o_wslides_nav_navbar_right {
padding-left: $spacer;
margin-left: auto;
border-left: 1px solid $border-color;
}
}
}
.o_quiz_js_quiz {
i.o_quiz_js_quiz_icon {
cursor: pointer;
}
i.o_quiz_js_quiz_icon:hover {
color: black !important;
}
}
.o_quiz_js_quiz_question {
.list-group-item {
font-size: 1rem;
input:checked + i.fa-circle {
color: $primary !important;
}
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
&.completed-disabled{
pointer-events: none;
}
}
a.o_quiz_js_quiz_is_correct {
color: black;
input:checked + i.fa-check-circle-o {
color: $primary !important;
}
}
.o_quiz_js_quiz_sequence_highlight {
background-color: #1252F3;
height: 1px;
z-index: 3;
&:before, &:after {
content: "";
@include size(6px);
display: block;
border-radius: 100%;
background-color: inherit;
@include o-position-absolute(-2px, -2px);
}
&:after {
right: auto;
left: -2px;
}
}
}
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="quiz.main">
<div class="h-100 w-100 overflow-auto px-2 py-2">
<div class="container">
<div t-foreach="widget.quiz.questions" t-as="question"
t-attf-class="o_quiz_js_quiz_question mt-3 mb-4 #{widget.track.completed ? 'completed-disabled' : ''}"
t-att-data-question-id="question.id" t-att-data-title="question.question">
<div class="h4">
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/>
</div>
<div class="list-group">
<t t-foreach="question.answer_ids" t-as="answer">
<a t-att-data-answer-id="answer.id" href="#"
t-att-data-text="answer.text_value"
t-attf-class="o_quiz_quiz_answer list-group-item d-flex align-items-center list-group-item-action #{widget.track.completed &amp;&amp; answer.is_correct ? 'list-group-item-success' : '' }">
<label class="my-0 d-flex align-items-center justify-content-center mr-2">
<input type="radio"
t-att-name="question.id"
t-att-value="answer.id"
class="d-none"/>
<i t-att-class="'fa fa-circle text-400' + (!(widget.track.completed &amp;&amp; answer.is_correct) ? '' : ' d-none')"></i>
<i class="fa fa-times-circle text-danger d-none"></i>
<i t-att-class="'fa fa-check-circle text-success' + (widget.track.completed &amp;&amp; answer.is_correct ? '' : ' d-none')"></i>
</label>
<span t-esc="answer.text_value"/>
</a>
</t>
<div class="o_quiz_quiz_answer_info list-group-item list-group-item-info d-none">
<i class="fa fa-info-circle"/>
<span class="o_quiz_quiz_answer_comment"/>
</div>
</div>
</div>
<div t-if="!widget.track.completed" class="o_quiz_js_quiz_validation border-top pt-3"/>
<div t-else="" class="row">
<div class="o_quiz_js_quiz_validation col py-2 bg-100 mb-2 border-bottom"/>
</div>
</div>
</div>
</t>
<t t-name="quiz.validation">
<div id="validation">
<div class="d-flex align-items-center justify-content-between">
<div t-att-class="'d-flex align-items-center' + (widget.track.completed ? ' alert alert-success my-0 py-1 px-3' : '')">
<button t-if="!widget.track.completed" role="button" title="Check answers" aria-label="Check answers"
class="btn btn-primary text-uppercase font-weight-bold o_quiz_js_quiz_submit">Check your answers</button>
<b t-else="" class="my-0 h5">Done !</b>
<span class="my-0 h5" style="line-height: 1">
<span t-if="widget.track.completed" role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2">
+ <t t-esc="widget.quiz.quizPointsGained || 0"/> Points
</span>
</span>
</div>
<div class="flex-grow-1 text-right">
<button t-if="widget.track.isManager"
class="d-none d-md-inline-block btn btn-light border o_quiz_js_quiz_reset">
Reset
</button>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<odoo><data>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Event Track Quiz Frontend Assets">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_event_track_quiz/static/src/scss/event_quiz.scss" t-ignore="true"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_event_track_quiz/static/src/js/event_quiz.js"/>
<script type="text/javascript" src="/website_event_track_quiz/static/src/js/event_quiz_leaderboard.js"/>
</xpath>
</template>
</data></odoo>
@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="event_leaderboard" name="Leaderboard">
<t t-call="website_event.layout">
<div t-if="visitors" class="bg-light pt32 pb32 o_wevent_quiz_leaderboard">
<t t-call="website_event_track_quiz.leaderboard_search_bar"/>
<div class="container mt32">
<div t-if="not search" class="row mb-3">
<div class="col-md-4 d-flex flex-grow-1" t-foreach="top3_visitors" t-as="visitor">
<t t-call="website_event_track_quiz.top3_visitor_card"></t>
</div>
</div>
<table class="table table-sm">
<tr t-foreach="visitors" t-as="visitor"
t-attf-class="#{'alert-info' if visitor['visitor'] == current_visitor else 'bg-white'} #{'o_wevent_quiz_scroll_to' if scroll_to_position and visitor['visitor'] == current_visitor else ''}">
<t t-call="website_event_track_quiz.all_visitor_card"/>
</tr>
</table>
<div class="form-inline justify-content-center">
<t t-call="website_event_track_quiz.pager_nobox"/>
</div>
</div>
</div>
<div t-if="not visitors and search" class="container mt32">
<t t-call="website_event_track_quiz.leaderboard_search_bar"/>
<div class='alert alert-warning mt32'>No user found for <strong><t t-esc="search"/></strong>. Try another search.</div>
</div>
<div t-if="not visitors and not search" class="vh-100 bg-light d-flex justify-content-center align-items-center">
<h4 class="text-muted font-weight-bold">There is currently no leaderboard available</h4>
</div>
</t>
</template>
<template id="pager_nobox" name="Pager (not box display)">
<ul t-if="pager['page_count'] > 1" t-attf-class="o_wprofile_pager font-weight-bold pagination m-0">
<li t-attf-class="page-item o_wprofile_pager_arrow #{'disabled' if pager['page']['num'] == 1 else ''}">
<a t-att-href=" pager['page_first']['url'] if pager['page']['num'] != 1 else None" class="page-link"><i class="fa fa-step-backward"/></a>
</li>
<li t-attf-class="page-item o_wprofile_pager_arrow #{'disabled' if pager['page']['num'] == 1 else ''}">
<a t-att-href=" pager['page_previous']['url'] if pager['page']['num'] != 1 else None" class="page-link"><i class="fa fa-caret-left"/></a>
</li>
<t t-foreach="pager['pages']" t-as="page">
<li t-attf-class="page-item #{'active disabled bg-primary rounded-circle' if page['num'] == pager['page']['num'] else ''}"> <a t-att-href="page['url']" class="page-link" t-raw="page['num']"></a></li>
</t>
<li t-attf-class="page-item o_wprofile_pager_arrow #{'disabled' if pager['page']['num'] == pager['page_count'] else ''}">
<a t-att-href="pager['page_next']['url'] if pager['page']['num'] != pager['page_count'] else None" class="page-link"><i class="fa fa-caret-right"/></a>
</li>
<li t-attf-class="page-item o_wprofile_pager_arrow #{'disabled' if pager['page']['num'] == pager['page_count'] else ''}">
<a t-att-href=" pager['page_last']['url'] if pager['page']['num'] != pager['page_count'] else None" class="page-link"><i class="fa fa-step-forward"/></a>
</li>
</ul>
</template>
<template id="top3_visitor_card" name="Top 3 Visitor Card">
<div class="card w-100 text-center mb-2 border-bottom-0">
<div class="card-body">
<div class="d-inline-block position-relative">
<img class="rounded-circle img-fluid"
style="width: 128px; height: 128px; object-fit: cover;"
t-att-src="image_data_uri(visitor['visitor'].partner_image) if visitor['visitor'].partner_image else '/web/static/src/img/user_placeholder.jpg'"/>
<img class="position-absolute" t-attf-src="/website_profile/static/src/img/rank_#{visitor['position']}.svg" alt="User rank" style="bottom: 0; right: -10px"/>
</div>
<h3 class="mt-2 mb-0" t-esc="visitor['visitor'].display_name"></h3>
</div>
<div class="row mx-0 o_wprofile_top3_card_footer text-nowrap">
<div class="col py-3"><b t-esc="visitor['points']"/> <span class="text-muted">Points</span></div>
</div>
</div>
</template>
<template id="all_visitor_card" name="All VIsitor Card">
<td class="align-middle text-right text-muted" style="width: 0">
<span t-esc="visitor['position']"/>
</td>
<td class="align-middle d-none d-sm-table-cell">
<img class="o_object_fit_cover rounded-circle o_wprofile_img_small"
width="30"
height="30"
t-att-src="image_data_uri(visitor['visitor'].partner_image) if visitor['visitor'].partner_image else '/web/static/src/img/user_placeholder.jpg'"/>
</td>
<td class="align-middle w-md-75">
<span class="font-weight-bold" t-esc="visitor['visitor'].display_name"/><br/>
</td>
<td class="align-middle font-weight-bold text-right text-nowrap">
<b t-esc="visitor['points']"/> <span class="text-muted small font-weight-bold">Points</span>
</td>
</template>
<!-- Sub nav -->
<template id="leaderboard_search_bar" name="Leaderboard search bar">
<div class="container">
<div class="row align-items-center justify-content-between">
<!-- Desktop Mode -->
<div class="col d-none d-md-flex flex-row align-items-center justify-content-end">
<!-- search -->
<form t-attf-action="#{'/event/%s/community/leaderboard/results' % (slug(event))}" role="search" method="get">
<div class="input-group ml-1 position-relative">
<span class="input-group-prepend">
<button class="btn btn-link text-white rounded-0 pr-1" type="submit" aria-label="Search" title="Search">
<i class="fa fa-search"></i>
</button>
</span>
<input type="text" class="form-control rounded-0" name="search" placeholder="Search Attendees" t-att-value="searched_name or ''"/>
</div>
</form>
</div>
<!-- Mobile Mode -->
<div class="col d-md-none py-1 o_wprofile_user_profile_sub_nav_mobile_col">
<div class="btn-group w-100 position-relative" role="group" aria-label="Mobile sub-nav">
<div class="btn-group ml-1 position-static mr-2">
<a class="btn bg-black-25 text-white dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-search"></i></a>
<div class="dropdown-menu dropdown-menu-right w-100" style="right: 10px;">
<form class="px-3" t-attf-action="#{'/event/%s/community/leaderboard' % (slug(event))}" role="search" method="get">
<div class="input-group">
<input type="text" class="form-control" name="search" placeholder="Search courses"/>
<span class="input-group-append">
<button class="btn btn-primary" type="submit" aria-label="Search" title="Search">
<i class="fa fa-search"/>
</button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</odoo>
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<odoo><data>
<menuitem id="event_quiz_menu"
name="Quizzes"
action="event_quiz_action"
parent="event.menu_event_configuration"
groups="base.group_no_one"
sequence="50"/>
<menuitem id="event_quiz_question_menu"
name="Quiz Questions"
action="event_quiz_question_action"
parent="event.menu_event_configuration"
groups="base.group_no_one"
sequence="55"/>
</data></odoo>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_quiz_question_view_search" model="ir.ui.view">
<field name="name">event.quiz.question.view.search</field>
<field name="model">event.quiz.question</field>
<field name="arch" type="xml">
<search string="Quiz Questions">
<field name="name"/>
<field name="quiz_id"/>
<group string="Group By" expand="0">
<filter string="Quiz" name="groupby_quiz_id" context="{'group_by': 'quiz_id'}"/>
</group>
</search>
</field>
</record>
<record id="event_quiz_question_view_tree" model="ir.ui.view">
<field name="name">event.quiz.question.view.tree</field>
<field name="model">event.quiz.question</field>
<field name="arch" type="xml">
<tree string="Quiz Questions">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="quiz_id"/>
<field name="awarded_points"/>
</tree>
</field>
</record>
<record id="event_quiz_question_view_tree_from_quiz" model="ir.ui.view">
<field name="name">event.quiz.question.view.tree.from.quiz</field>
<field name="model">event.quiz.question</field>
<field name="inherit_id" ref="website_event_track_quiz.event_quiz_question_view_tree"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='quiz_id']" position="replace">
</xpath>
</field>
</record>
<record id="event_quiz_question_view_form" model="ir.ui.view">
<field name="name">event.quiz.question.view.form</field>
<field name="model">event.quiz.question</field>
<field name="arch" type="xml">
<form string="Quiz Question">
<sheet>
<h1>
<field name="name" default_focus="1"
placeholder="e.g. According to Douglas, what should you pay most attention to?"/>
</h1>
<group>
<field name="quiz_id"/>
<field name="awarded_points" invisible="1"/>
</group>
<group name="questions">
<field name="answer_ids" nolabel="1">
<tree editable="bottom" create="true" delete="true">
<field name="sequence" widget="handle"/>
<field name="text_value"/>
<field name="awarded_points"/>
<field name="comment"/>
</tree>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="event_quiz_question_view_form_from_quiz" model="ir.ui.view">
<field name="name">event.quiz.question.view.form.from.quiz</field>
<field name="model">event.quiz.question</field>
<field name="inherit_id" ref="website_event_track_quiz.event_quiz_question_view_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='quiz_id']" position="replace">
</xpath>
</field>
</record>
<record id="event_quiz_question_action" model="ir.actions.act_window">
<field name="name">Event Quiz Questions</field>
<field name="res_model">event.quiz.question</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Question
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="quiz_content" name="Track: Quiz specific content">
<t t-set="quiz_completed" t-value="quiz_completed or False"/>
<div class="o_quiz_js_quiz col"
t-att-data-id="track.id"
t-att-data-event-id="track.event_id.id"
t-att-data-completed="1 if quiz_completed else 0"
t-att-data-quiz-attempts-count="quiz_attempts_count or 0"
t-att-data-quiz-points-gained="quiz_points"
t-att-data-is-manager="user_event_manager or 0">
<t t-foreach="track.quiz_id.question_ids" t-as="question">
<t t-call="website_event_track_quiz.quiz_question"/>
</t>
<div class="o_quiz_js_quiz_validation pt-3"/>
</div>
</template>
<template id="quiz_question" name="Quiz question template">
<div t-att-class="'o_quiz_js_quiz_question mt-3 %s' % ('completed-disabled' if quiz_completed else '')"
t-att-data-question-id="question['id']" t-att-data-title="question['name']" >
<div class="row d-flex mb-2 mx-0">
<div class="h4">
<span t-esc="question['name']"/>
</div>
</div>
<div class="list-group">
<t t-foreach="question['answer_ids']" t-as="answer">
<a t-att-data-answer-id="answer['id']" href="#"
t-att-data-text="answer['text_value']" t-att-data-is-correct="answer['is_correct']" t-att-data-comment="answer['comment']"
t-att-class="'o_quiz_quiz_answer list-group-item list-group-item-action d-flex align-items-center %s' % ('list-group-item-success' if slid_completed and answer['is_correct'] else '')">
<label class="my-0 d-flex align-items-center justify-content-center mr-2">
<input type="radio"
t-att-name="question['id']"
t-att-value="answer['id']"
class="d-none"
t-att-disabled="quiz_completed"/>
<i t-att-class="'fa fa-circle text-400 %s' % ('d-none' if quiz_completed and answer['is_correct'] else '')"/>
<i class="fa fa-times-circle text-danger d-none"></i>
<i t-att-class="'fa fa-check-circle text-success %s' % ('d-none' if not (quiz_completed and answer['is_correct']) else '')"></i>
</label>
<span t-esc="answer['text_value']"/>
</a>
</t>
<div class="o_quiz_quiz_answer_info list-group-item list-group-item-info d-none">
<i class="fa fa-info-circle"/>
<span class="o_quiz_quiz_answer_comment"/>
</div>
</div>
</div>
</template>
</odoo>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_quiz_view_search" model="ir.ui.view">
<field name="name">event.quiz.view.search</field>
<field name="model">event.quiz</field>
<field name="arch" type="xml">
<search string="Quizzes">
<field name="name"/>
<field name="event_track_id"/>
<field name="event_id"/>
<group string="Group By" expand="0">
<filter string="Track" name="groupby_event_track_id" context="{'group_by': 'event_track_id'}"/>
<filter string="Event" name="groupby_event_id" context="{'group_by': 'event_id'}"/>
</group>
</search>
</field>
</record>
<record id="event_quiz_view_tree" model="ir.ui.view">
<field name="name">event.quiz.view.tree</field>
<field name="model">event.quiz</field>
<field name="arch" type="xml">
<tree string="Quizzes">
<field name="name"/>
<field name="event_id"/>
<field name="event_track_id"/>
</tree>
</field>
</record>
<record id="event_quiz_view_form" model="ir.ui.view">
<field name="name">event.quiz.view.form</field>
<field name="model">event.quiz</field>
<field name="arch" type="xml">
<form string="Quiz">
<sheet>
<h1>
<field name="name" default_focus="1"
placeholder="e.g. Test your Knowledge"/>
</h1>
<group>
<field name="event_track_id"/>
<field name="event_id"/>
</group>
<group name="questions" string="Questions">
<field name="question_ids" nolabel="1"
context="{
'tree_view_ref': 'website_event_track_quiz.event_quiz_question_view_tree_from_quiz',
'form_view_ref': 'website_event_track_quiz.event_quiz_question_view_form_from_quiz'
}"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="event_quiz_view_form_from_track" model="ir.ui.view">
<field name="name">event.quiz.view.form</field>
<field name="model">event.quiz</field>
<field name="inherit_id" ref="website_event_track_quiz.event_quiz_view_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='event_track_id']" position="replace"></xpath>
<xpath expr="//field[@name='event_id']" position="replace"></xpath>
</field>
</record>
<record id="event_quiz_action" model="ir.actions.act_window">
<field name="name">Event Quizzes</field>
<field name="res_model">event.quiz</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Quiz
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="event_track_content"
name="Track: Main Description: add quiz"
inherit_id="website_event_track_session.event_track_content">
<xpath expr="//div[hasclass('o_wesession_track_main_description')]" position="after">
<div id="we_track_quiz_container" t-if="track.quiz_id"
t-att-class="'o_quiz_js_quiz_container o_quiz_main border-top bg-white px-2 pt-4 pb-4 col-12 %s' % ('' if track.is_quiz_completed else 'd-none')"
t-att-data-object-id="track.id">
<h3 class="col-12">Quiz</h3>
<t t-call="website_event_track_quiz.quiz_content">
<t t-set="track" t-value="track"/>
<t t-set="quiz_completed" t-value="track.is_quiz_completed"/>
<t t-set="quiz_points" t-value="track.quiz_points"/>
</t>
</div>
</xpath>
<xpath expr="//div[hasclass('o_we_track_reminder_button')]" position="before">
<div class="o_we_track_quiz_button mr-2 my-1" t-if="track.quiz_id and not track.is_quiz_completed and not track.is_track_upcoming">
<a class="btn btn-primary" href="#we_track_quiz_container" onclick="$('.o_quiz_js_quiz_container').removeClass('d-none'); ">
Take the Quiz
</a>
</div>
</xpath>
</template>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<odoo>
<record id="event_track_view_form" model="ir.ui.view" >
<field name="name">event.track.view.form.inherit.event.track.quiz</field>
<field name="model">event.track</field>
<field name="inherit_id" ref="website_event_track_session.event_track_view_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='event_track_cta_group']" position="after">
<group name="event_track_quiz_group">
<field name="quiz_id"
context="{'form_view_ref': 'website_event_track_quiz.event_quiz_view_form_from_track'}"/>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_type_view_form" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.meet</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="website_event.event_type_view_form_inherit_website"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='community-menu']" position="attributes">
<attribute name="groups"></attribute>
</xpath>
</field>
</record>
</data></odoo>