Skip to main content

Quizzes

//! # Quizzes Unit Pallet
//!
//! Pallet that allows users to create quizzes and questions for other users to answer.
//!
//! ## Overview
//!
//! Quizzes module provides functionality to users to create quizzes, add questions inside
//! a quiz, delete questions, delete an entire quiz with its associated questions, and submit reponses.
//!
//! The supported dispatchable functions are documented in the [`Call`] enum.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a quiz.
//! * Create a question in a quiz by the quiz´s creator.
//! * Submit response to a question.
//! * Upvote a post comment.
//! * Delete a question in a quiz by the quiz´s creator.
//! * Delete quiz by the quiz´s creator.
//!
//! ## Interface
//!
//! //! ### Permissionless Functions
//!
//! * `create_quiz`: Creates a new quiz associated to an asset.
//! * `submit_response`: Submit the response for a question.
//!
//! ### Privileged Functions
//!
//! * `create_question`: Add a new question in a quiz, called by the quiz´s creator.
//! * `delete_question`: Remove a question from a quiz, called by the quiz´s creator.
//! * `delete_quiz`: Delete a quiz and all its questions, called by the quiz´s creator.
//!
//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

#[frame_support::pallet]
pub mod pallet {
use frame_support::{ pallet_prelude::*, traits::fungibles };
use frame_system::pallet_prelude::*;
use traits::{ subaccounts::{ SubAccounts, AccountOrigin } };

pub type AssetIdOf<T> =
<<T as Config>::Fungibles as fungibles::Inspect<<T as frame_system::Config>::AccountId>>::AssetId;

#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);

#[pallet::config]
pub trait Config: frame_system::Config + pallet_assets::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

/// Type to access the Assets Pallet.
type Fungibles: fungibles::Inspect<Self::AccountId>;

/// Max length of string
#[pallet::constant]
type MaxLength: Get<u32>;

/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
}

/// Data for a quiz.
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Debug, Eq, PartialEq, Clone, Default)]
#[scale_info(skip_type_params(S))]
pub struct Quiz<AccountId, S: Get<u32>> {
id: u32,
creator: AccountId,
title: BoundedVec<u8, S>,
}

/// Information related to a question.
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, Debug, Eq, PartialEq, Clone, Default)]
#[scale_info(skip_type_params(S))]
pub struct Question<S: Get<u32>> {
pub question: BoundedVec<u8, S>,
pub option_1: BoundedVec<u8, S>,
pub option_2: BoundedVec<u8, S>,
pub option_3: BoundedVec<u8, S>,
pub option_4: BoundedVec<u8, S>,
pub question_id: u32,
pub correct_option: u8,
pub explanation: BoundedVec<u8, S>,
}

#[pallet::storage]
/// Counter for quizzes.
#[pallet::getter(fn quiz_counter)]
pub type QuizCounter<T: Config> = StorageValue<_, u32, ValueQuery>;

#[pallet::storage]
/// Counter for questions.
#[pallet::getter(fn question_counter)]
pub type QuestionIdCounter<T: Config> = StorageValue<_, u32, ValueQuery>;

#[pallet::storage]
/// Information about a quiz for an asset.
#[pallet::getter(fn quizzes)]
pub type Quizzes<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
u32, //quiz id
Quiz<T::AccountId, T::MaxLength>
>;

#[pallet::storage]
/// Information about a question.
/// The first key is the asset, the second key is the quiz, and the third is the question id.
#[pallet::getter(fn questions)]
pub type Questions<T: Config> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, AssetIdOf<T>>,
NMapKey<Blake2_128Concat, u32>, // quiz id
NMapKey<Blake2_128Concat, u32>, // question id
),
Question<T::MaxLength>
>;

#[pallet::storage]
/// User´s answer for a specific question.
/// The first key is the user account, the second key is the quiz id, and the third is the question id.
#[pallet::getter(fn answers)]
pub type Answer<T: Config> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, T::AccountId>,
NMapKey<Blake2_128Concat, u32>, // quiz id
NMapKey<Blake2_128Concat, u32>, // question id
),
u8
>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// New quiz created
/// [asset_id, quiz_id]
QuizCreated {
asset_id: AssetIdOf<T>,
quiz_id: u32,
},
/// New question has been created
/// [asset_id, quiz_id, question_id]
QuestionCreated {
asset_id: AssetIdOf<T>,
quiz_id: u32,
question_id: u32,
},
/// A new answer has been submitted
/// [quiz_id, question_id, who]
AnswerSubmitted {
quiz_id: u32,
question_id: u32,
who: T::AccountId,
},
/// A question has been deleted
QuestionDeleted {
quiz_id: u32,
question_id: u32,
},
/// A quiz and the related question has been deleted
QuizDeleted {
asset_id: AssetIdOf<T>,
quiz_id: u32,
},
}

#[pallet::error]
pub enum Error<T> {
/// Origin is not the creator of the quiz
NotQuizCreator,
/// There is no quiz created for the given keys
NoQuizFound,
/// There is no question created for the given keys
NoQuestionFound,
/// Option out of range
InvalidOption,
/// Cannot answer a question twice
AnswerAlreadySubmitted,
}

#[pallet::call]
impl<T: Config> Pallet<T> {
/// The origin can create a quiz for a particular asset.
///
/// The origin must be Signed.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to create the quiz.
/// - `title`: The title of the quiz.
///
/// Emits `QuizCreated` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(0)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn create_quiz(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
title: BoundedVec<u8, T::MaxLength>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

let quiz_counter = QuizCounter::<T>::get();

let quiz = Quiz {
id: quiz_counter.clone(),
creator: who,
title,
};

Quizzes::<T>::insert(&asset_id, &quiz_counter, quiz);

QuizCounter::<T>::mutate(|n| {
*n += 1;
});

Self::deposit_event(Event::QuizCreated { asset_id, quiz_id: quiz_counter });

// Return a successful DispatchResultWithPostInfo
Ok(())
}

/// The origin can create a question for a quiz for a particular asset.
///
/// The origin must be Signed and the sender should be the creator of the quiz.
///
/// The quiz should exist.
///
/// The correct option should be a number from 1 to 4.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to create the question.
/// - `quiz_id`: The identifier of the quiz to create the question.
/// - `question`: The new question for the quiz.
/// - `option_1`: The first option that the users can choose.
/// - `option_2`: The second option that the users can choose.
/// - `option_3`: The third option that the users can choose.
/// - `option_4`: The fourth option that the users can choose.
/// - `explanation`: Explanation about the answer.
/// - `correct_option`: The correct anwser for this question.
///
/// Emits `QuestionCreated` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(1)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn create_question(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
quiz_id: u32,
question: BoundedVec<u8, T::MaxLength>,
option_1: BoundedVec<u8, T::MaxLength>,
option_2: BoundedVec<u8, T::MaxLength>,
option_3: BoundedVec<u8, T::MaxLength>,
option_4: BoundedVec<u8, T::MaxLength>,
explanation: BoundedVec<u8, T::MaxLength>,
correct_option: u8
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

let quiz = Quizzes::<T>::get(&asset_id, &quiz_id).ok_or(Error::<T>::NoQuizFound)?;

ensure!(quiz.creator == who, Error::<T>::NotQuizCreator);

let question_id = QuestionIdCounter::<T>::get();

QuestionIdCounter::<T>::mutate(|n| {
*n += 1;
});

let new_question = Question {
question,
option_1,
option_2,
option_3,
option_4,
question_id: question_id.clone(),
correct_option,
explanation,
};

Questions::<T>::insert((&asset_id, &quiz_id, &question_id), new_question);

Self::deposit_event(Event::QuestionCreated { asset_id, quiz_id, question_id });

Ok(())
}

/// The origin can submit a respone for a question in a particular quiz.
///
/// The origin must be Signed.
///
/// The question should exist.
///
/// The users can submit a response once for question.
///
/// The correct selected should be a number from 1 to 4.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to submit the answer.
/// - `quiz_id`: The identifier of the quiz to submit the answer.
/// - `question_id`: The identifier of the question to submit the answer.
/// - `selected_option`: The selected option by the origin.
///
/// Emits `AnswerSubmitted` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(2)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn submit_response(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
quiz_id: u32,
question_id: u32,
selected_option: u8
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

ensure!(
Questions::<T>::contains_key((&asset_id, &quiz_id, &question_id)),
Error::<T>::NoQuestionFound
);
ensure!(selected_option <= 4 && selected_option >= 1, Error::<T>::InvalidOption);
ensure!(
!Answer::<T>::contains_key((&who, &quiz_id, &question_id)),
Error::<T>::AnswerAlreadySubmitted
);

Answer::<T>::insert((&who, &quiz_id, &question_id), selected_option);

Self::deposit_event(Event::AnswerSubmitted {
quiz_id,
question_id,
who,
});

Ok(())
}

/// The origin can delete a question in a particular quiz.
///
/// The origin must be Signed and the sender should be the creator of the quiz.
///
/// The question and the quiz should exist.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to delete the question.
/// - `quiz_id`: The identifier of the quiz to delete the question.
/// - `question_id`: The identifier of the question to delete the question.
///
/// Emits `QuestionDeleted` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(3)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn delete_question(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
quiz_id: u32,
question_id: u32
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

let quiz = Quizzes::<T>::get(&asset_id, &quiz_id).ok_or(Error::<T>::NoQuizFound)?;

ensure!(quiz.creator == who, Error::<T>::NotQuizCreator);

Questions::<T>::remove((&asset_id, &quiz_id, &question_id));

Self::deposit_event(Event::QuestionDeleted { quiz_id, question_id });

Ok(())
}

// TODO: Delete answers after deleting a quiz or a question
/// The origin can delete a quiz in a particular quiz.
///
/// The origin must be Signed and the sender should be the creator of the quiz.
///
/// The quiz should exist.
///
/// If the quiz has questions created, they will be deleted.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to delete the quiz.
/// - `quiz_id`: The identifier of the quiz to delete the quiz.
///
/// Emits `QuizDeleted` event when successful.
///
/// Weight: `O(1)` TODO: Add correct weight
#[pallet::call_index(4)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn delete_quiz(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
quiz_id: u32
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account to store the info
who = <T as Config>::SubAccounts::get_main_account(who)?;

let quiz = Quizzes::<T>::get(&asset_id, &quiz_id).ok_or(Error::<T>::NoQuizFound)?;

ensure!(quiz.creator == who, Error::<T>::NotQuizCreator);

Quizzes::<T>::remove(&asset_id, &quiz_id);

let _result = Questions::<T>::clear_prefix((&asset_id, &quiz_id), 10, None);

Self::deposit_event(Event::QuizDeleted { asset_id, quiz_id });

Ok(())
}
}
}