Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions website_event_questions_multiple/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Questions on Events - Type multiple

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allows to add new question type : Selection multiple which allows
This module allows to add new question type : Multiple Selection which allows
attendees to select multiple answers to a question.

**Table of contents**
Expand All @@ -39,7 +39,7 @@ attendees to select multiple answers to a question.
Configuration
=============

On an event, when creating a new question, you can select new type : Selection multiple
On an event, when creating a new question, you can select new type : Multiple Selection

Bug Tracker
===========
Expand Down
3 changes: 3 additions & 0 deletions website_event_questions_multiple/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"views/event_questions_views.xml",
"views/event_registration_views.xml",
],
"assets": {
"web.assets_frontend": ["website_event_questions_multiple/static/src/**/*"],
},
"installable": True,
"auto_install": False,
}
90 changes: 72 additions & 18 deletions website_event_questions_multiple/controllers/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2023 Le Filament (https://le-filament.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

from odoo.exceptions import UserError
from odoo.http import request

from odoo.addons.website_event_questions.controllers.main import WebsiteEvent
Expand All @@ -11,33 +12,86 @@ def _process_attendees_form(self, event, form_details):
"""Process data posted from the attendee details form.
Extracts question answers:
- For questions of type 'multiple_choice', extracting the suggested answer id"""
registrations = super(WebsiteEvent, self)._process_attendees_form(
event, form_details
registrations = super()._process_attendees_form(event, form_details)

# list all question_multi_answer with is_mandatory_answer
mandatory_multiple_general_question_answer_count = {}
mandatory_multiple_specific_question_ids = set()
filter = (
lambda q: q.question_type == "multiple_choice"
and q.is_mandatory_answer == True
)
for general_question in event.general_question_ids.filtered(filter):
mandatory_multiple_general_question_answer_count[general_question.id] = 0
for specific_question in event.specific_question_ids.filtered(filter):
mandatory_multiple_specific_question_ids.add(specific_question.id)

mandatory_multiple_specific_question_answer_count = [
{i: 0 for i in mandatory_multiple_specific_question_ids}
for j in range(len(registrations))
]

general_answer_ids = []
for key, _value in form_details.items():
if "question_multi_answer" in key:
dummy, registration_index, question_answer = key.split("-")
question_id, answer_id = question_answer.split("_")
question_sudo = request.env["event.question"].browse(int(question_id))
# test html input prefix
if key.startswith("question_multi_answer"):
_html_input_prefix, registration_index_str, question_answer = key.split(
"-"
)
registration_index = int(registration_index_str)
question_id_str, answer_id = question_answer.split("_")
question_id = int(question_id_str)
question_sudo = request.env["event.question"].browse(question_id)
answer_sudo = request.env["event.question.answer"].browse(
int(answer_id)
)
answer_values = None
if question_sudo.question_type == "multiple_choice":
answer_values = {
"question_id": int(question_id),
"value_text_box": answer_sudo.name,
}

if answer_values and not int(registration_index):
assert (
question_sudo.question_type == "multiple_choice"
) # otherwise, html is malformed
answer_values = {
"question_id": question_id,
"value_text_box": answer_sudo.name,
}
# question with null registration index are general
if registration_index == 0:
general_answer_ids.append((0, 0, answer_values))
elif answer_values:
registrations[int(registration_index) - 1][
"registration_answer_ids"
].append((0, 0, answer_values))
if question_sudo.is_mandatory_answer:
mandatory_multiple_general_question_answer_count[
question_id
] += 1
# question with registration index are specific to one registration
else:
rindex = registration_index - 1 # zero-based array ¹indexing
registrations[rindex]["registration_answer_ids"].append(
(0, 0, answer_values)
)
if question_sudo.is_mandatory_answer:
mandatory_multiple_specific_question_answer_count[rindex][
question_id
] += 1

# check that answers contain at least one for mandatory question
# general
for q, c in mandatory_multiple_general_question_answer_count.items():
if c == 0:
raise UserError(
"Question "
+ str(q)
+ " is mandatory but did not receive an answer."
)
# specific
for r, cc in enumerate(mandatory_multiple_specific_question_answer_count):
for q, c in cc.items():
if c == 0:
raise UserError(
"Question "
+ str(q)
+ " is mandatory but did not receive an answer in ticket number "
+ str(r)
+ "."
)

# append general question to all items
for registration in registrations:
registration["registration_answer_ids"].extend(general_answer_ids)

Expand Down
36 changes: 36 additions & 0 deletions website_event_questions_multiple/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_event_questions_multiple
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-06 11:52+0000\n"
"PO-Revision-Date: 2025-03-06 11:52+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: website_event_questions_multiple
#: model:ir.model,name:website_event_questions_multiple.model_event_question
msgid "Event Question"
msgstr "Question"

#. module: website_event_questions_multiple
#: model:ir.model.fields.selection,name:website_event_questions_multiple.selection__event_question__question_type__multiple_choice
msgid "Multiple Selection"
msgstr "Sélection multiple"

#. module: website_event_questions_multiple
#: model:ir.model.fields,field_description:website_event_questions_multiple.field_event_question__question_type
msgid "Question Type"
msgstr "Type de question"

#. module: website_event_questions_multiple
#: model_terms:ir.ui.view,arch_db:website_event_questions_multiple.registration_event_question
msgid "Please select at least one checkbox."
msgstr "Veuillez cocher au moins une case."
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ <h1 class="title">Questions on Events - Type multiple</h1>
!! source digest: sha256:de10ccad1e53385cb3c9aa56bc533e4cd053db148e2d5fec32a0a0e04ac0e690
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/event/tree/16.0/website_event_questions_multiple"><img alt="OCA/event" src="https://img.shields.io/badge/github-OCA%2Fevent-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/event-16-0/event-16-0-website_event_questions_multiple"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/event&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to add new question type : Selection multiple which allows
<p>This module allows to add new question type : Multiple Selection which allows
attendees to select multiple answers to a question.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
Expand All @@ -388,7 +388,7 @@ <h1 class="title">Questions on Events - Type multiple</h1>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>On an event, when creating a new question, you can select new type : Selection multiple</p>
<p>On an event, when creating a new question, you can select new type : Multiple Selection</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
Expand Down
163 changes: 163 additions & 0 deletions website_event_questions_multiple/static/src/js/form_validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/** @odoo-module **/

import publicWidget from "web.public.widget";
import EventRegistrationForm from "website_event.website_event";

import ajax from "web.ajax";

/// declare widget
// extends from EventRegistrationForm from ocb/addons/website_event/static/src/js/website_event.js
export const EventRegistrationFormWithValidation = EventRegistrationForm.extend({
/// ------------------------------------------------
/// WARNING: code duplication for lack of extensibility
// this is a copy of the on_click function of the parent class
// which is the one opening the modal with these steps:
// 1. when clicking on "Register button"
// 2. look at selection (number of tickets to register)
// 3. disable the register button if no valid answer (e.g. zero tickets)
// 4. make an ajax.jsonRpc call to fetch the modal
// 5. inject the modal inside the page (there can be multiple instances of the modal if the register button is clicked multiple times)
// the only added behavior is to return the modal class to be able to add form validation logic
// return value is a Promise<modal | undefined>
on_click_parent: async function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $form = $(ev.currentTarget).closest("form");
var $button = $(ev.currentTarget).closest('[type="submit"]');
var post = {};
$("#registration_form table").siblings(".alert").remove();
$("#registration_form select").each(function () {
post[$(this).attr("name")] = $(this).val();
});
var tickets_ordered = _.some(
_.map(post, function (value, key) {
return parseInt(value);
})
);
if (!tickets_ordered) {
$('<div class="alert alert-info"/>')
.text(_t("Please select at least one ticket."))
.insertAfter("#registration_form table");
return new Promise(function () {});
} else {
$button.attr("disabled", true);
var action = $form.data("action") || $form.attr("action");
var self = this;
return ajax.jsonRpc(action, "call", post).then(async function (modal) {
const tokenObj = await self._recaptcha.getToken(
"website_event_registration"
);
if (tokenObj.error) {
self.displayNotification({
type: "danger",
title: _t("Error"),
message: tokenObj.error,
sticky: true,
});
$button.prop("disabled", false);
return false;
}
var $modal = $(modal);
$modal.find(".modal-body > div").removeClass("container"); // retrocompatibility - REMOVE ME in master / saas-19
$modal.appendTo(document.body);
const modalBS = new Modal($modal[0], {
backdrop: "static",
keyboard: false,
});
modalBS.show();
$modal.appendTo("body").modal("show");
$modal.on("click", ".js_goto_event", function () {
$modal.modal("hide");
$button.prop("disabled", false);
});
$modal.on("click", ".btn-close", function () {
$button.prop("disabled", false);
});
$modal.on("submit", "form", function (ev) {
const tokenInput = document.createElement("input");
tokenInput.setAttribute("name", "recaptcha_token_response");
tokenInput.setAttribute("type", "hidden");
tokenInput.setAttribute("value", tokenObj.token);
ev.currentTarget.appendChild(tokenInput);
});
// THIS IS THE ONLY REAL MODIFICATION
// return the $modal jQuery object
return $modal;
});
}
},
/// ------------------------------------------------

/**
* @override
* override the parent method to replace call to the modified function
*/
on_click: async function (ev) {
// get modal from copy (not super())
const $modal = await this.on_click_parent(ev);
if ($modal) {
this.add_validation($modal);
} else {
console.log("No modal was added.");
}
},

// this is where I add validation to the form in the modal
add_validation: function ($modal) {
console.log("Adding validation to modal.");

// prevent default
$modal.on("submit", "form", function (ev) {
console.log("form submitted");

// search all check groups with mandatory answers
$modal
.find("div.form-check-group.is_mandatory_answer")
.each(function (index) {
console.log("testing group", index);
// count number of checkbox
let checked_count = 0;
$(this)
.find(".form-check-input")
.each(function () {
if ($(this).prop("checked")) checked_count++;
});

// if zero, prevent default and display message
if (checked_count == 0) {
console.log("at least one checkbox must be checked");
$(this).find(".mandatory-message").removeClass("d-none");
ev.preventDefault();
ev.stopPropagation();
}
});
});
},
});

/// register widget
// (also copied from parent)
publicWidget.registry.EventRegistrationFormWithValidationInstance =
publicWidget.Widget.extend({
selector: "#registration_form",

/**
* @override
*/
start: function () {
console.log("instance start override");
var def = this._super.apply(this, arguments);
this.instance = new EventRegistrationFormWithValidation(this); // <--- here we instantiante child widget
return Promise.all([def, this.instance.attachTo(this.$el)]);
},
/**
* @override
*/
destroy: function () {
this.instance.setElement(null);
this._super.apply(this, arguments);
this.instance.setElement(this.$el);
},
});

console.log("form validation widget registered");
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
>
<xpath expr="//span" position="after">
<t t-if="question.question_type == 'multiple_choice'">
<div class="mb-4">
<div
t-attf-class="mb-4 form-check-group #{'is_mandatory_answer' if question.is_mandatory_answer else ''}"
>
<div class="mandatory-message alert alert-danger d-none">
Please select at least one checkbox.
</div>
<t t-foreach="question.answer_ids" t-as="answer">
<div class="form-check">
<input
Expand Down
Loading