#![cfg_attr(not(feature = "std"), no_std)] //! # Perwerde (Education) Pezpallet //! //! A pezpallet for managing educational courses, student enrollments, and achievement tracking. //! //! ## Overview //! //! The Perwerde pezpallet implements an on-chain educational platform where: //! - Educators create and manage courses with IPFS-linked content //! - Students enroll in courses and track their progress //! - Course completion earns points that contribute to trust scores //! - Educational achievements are permanently recorded on-chain //! //! ## Core Features //! //! ### Course Management //! - Admins create courses with name, description, and content links (IPFS) //! - Courses can be active or archived //! - Each course has a unique ID and owner //! - Course metadata is immutable after creation //! //! ### Student Enrollment //! - Students enroll in active courses //! - One enrollment per student per course //! - Enrollment history tracked with block numbers //! - Students can be enrolled in multiple courses simultaneously //! //! ### Completion & Points //! - Course owners mark student completions //! - Points awarded upon completion //! - Points contribute to Perwerde score for trust calculation //! - Completion timestamps recorded permanently //! //! ## Perwerde Score System //! //! The Perwerde score is derived from total education points: //! - Each completed course awards points //! - Points accumulate over time //! - Score used by `pezpallet-trust` for composite trust calculation //! - Higher education achievement improves ecosystem standing //! //! ## Interface //! //! ### Extrinsics //! //! - `create_course(name, description, content_link)` - Create new educational course (admin) //! - `enroll_student(course_id)` - Enroll in an active course (user) //! - `mark_course_completed(student, course_id, points)` - Award completion points (course owner) //! - `archive_course(course_id)` - Archive a course (course owner) //! //! ### Storage //! //! - `Courses` - Course metadata indexed by course ID //! - `NextCourseId` - Auto-incrementing course ID counter //! - `Enrollments` - Student enrollment records (student, course_id) → Enrollment //! - `StudentCourses` - Per-student list of enrolled course IDs //! //! ### Integration //! //! - Implements `PerwerdeScoreProvider` trait for `pezpallet-trust` //! - Education scores contribute to validator eligibility //! - Course completion history visible to governance //! //! ## Security Features //! //! - Only course owners can mark completions //! - Active courses required for enrollment //! - No duplicate enrollments //! - Maximum courses per student limit //! - Admin-only course creation //! //! ## Runtime Integration Example //! //! ```ignore //! impl pezpallet_perwerde::Config for Runtime { //! type RuntimeEvent = RuntimeEvent; //! type AdminOrigin = EnsureRoot; //! type WeightInfo = pezpallet_perwerde::weights::BizinikiwiWeight; //! type MaxCourseNameLength = ConstU32<128>; //! type MaxCourseDescLength = ConstU32<512>; //! type MaxCourseLinkLength = ConstU32<256>; //! type MaxStudentsPerCourse = ConstU32<100>; //! } //! ``` pub use pezpallet::*; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod weights; // These modules should only be compiled in `std` environment. #[cfg(all(feature = "std", any(test, feature = "runtime-benchmarks")))] pub mod mock; #[cfg(all(feature = "std", test))] mod tests; pub use weights::WeightInfo; #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_support::{ dispatch::DispatchResult, pezpallet_prelude::*, traits::{EnsureOrigin, Get}, }; use pezframe_system::pezpallet_prelude::*; #[pezpallet::pezpallet] pub struct Pezpallet(_); #[pezpallet::config] pub trait Config: pezframe_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type AdminOrigin: EnsureOrigin; type WeightInfo: WeightInfo; #[pezpallet::constant] type MaxCourseNameLength: Get; #[pezpallet::constant] type MaxCourseDescLength: Get; #[pezpallet::constant] type MaxCourseLinkLength: Get; #[pezpallet::constant] type MaxStudentsPerCourse: Get; /// Maximum number of courses a single student can enroll in /// Used for StudentCourses storage bound #[pezpallet::constant] type MaxCoursesPerStudent: Get; } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum CourseStatus { Active, Archived, } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Course { pub id: u32, pub owner: T::AccountId, pub name: BoundedVec, pub description: BoundedVec, pub content_link: BoundedVec, pub status: CourseStatus, pub created_at: BlockNumberFor, } #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Enrollment { pub student: T::AccountId, pub course_id: u32, pub enrolled_at: BlockNumberFor, pub completed_at: Option>, pub points_earned: u32, } #[pezpallet::storage] #[pezpallet::getter(fn courses)] pub type Courses = StorageMap<_, Blake2_128Concat, u32, Course, OptionQuery>; #[pezpallet::storage] #[pezpallet::getter(fn next_course_id)] pub type NextCourseId = StorageValue<_, u32, ValueQuery>; #[pezpallet::storage] #[pezpallet::getter(fn enrollments)] pub type Enrollments = StorageMap<_, Blake2_128Concat, (T::AccountId, u32), Enrollment, OptionQuery>; /// Per-student list of enrolled course IDs /// UPDATED (Gemini suggestion): Uses MaxCoursesPerStudent instead of MaxStudentsPerCourse /// This is the correct semantic - limits how many courses ONE student can take #[pezpallet::storage] #[pezpallet::getter(fn student_courses)] pub type StudentCourses = StorageMap< _, Blake2_128Concat, T::AccountId, BoundedVec, ValueQuery, >; #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { CourseCreated { course_id: u32, owner: T::AccountId }, StudentEnrolled { student: T::AccountId, course_id: u32 }, CourseCompleted { student: T::AccountId, course_id: u32, points: u32 }, CourseArchived { course_id: u32 }, } #[pezpallet::error] pub enum Error { CourseNotFound, AlreadyEnrolled, NotEnrolled, CourseNotActive, CourseAlreadyCompleted, NotCourseOwner, TooManyCourses, } #[pezpallet::call] impl Pezpallet { #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::create_course())] pub fn create_course( origin: OriginFor, name: BoundedVec, description: BoundedVec, content_link: BoundedVec, ) -> DispatchResult { let owner = T::AdminOrigin::ensure_origin(origin)?; let course_id = NextCourseId::::get(); // Parameters are already bounded, no conversion needed let course = Course { id: course_id, owner: owner.clone(), name, description, content_link, status: CourseStatus::Active, created_at: pezframe_system::Pezpallet::::block_number(), }; Courses::::insert(course_id, course); NextCourseId::::mutate(|id| *id += 1); Self::deposit_event(Event::CourseCreated { course_id, owner }); Ok(()) } #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::enroll())] pub fn enroll(origin: OriginFor, course_id: u32) -> DispatchResult { let student = ensure_signed(origin)?; let course = Courses::::get(course_id).ok_or(Error::::CourseNotFound)?; ensure!(course.status == CourseStatus::Active, Error::::CourseNotActive); ensure!( !Enrollments::::contains_key((&student, course_id)), Error::::AlreadyEnrolled ); let enrollment = Enrollment { student: student.clone(), course_id, enrolled_at: pezframe_system::Pezpallet::::block_number(), completed_at: None, points_earned: 0, }; Enrollments::::insert((&student, course_id), enrollment); StudentCourses::::try_mutate(&student, |courses| { courses.try_push(course_id).map_err(|_| Error::::TooManyCourses) })?; Self::deposit_event(Event::StudentEnrolled { student, course_id }); Ok(()) } /// Mark a student's course as completed and award points /// SECURITY: Only the course owner can mark completions, not students themselves #[pezpallet::call_index(2)] #[pezpallet::weight(T::WeightInfo::complete_course())] pub fn complete_course( origin: OriginFor, student: T::AccountId, course_id: u32, points: u32, ) -> DispatchResult { let caller = ensure_signed(origin)?; // Verify caller is the course owner let course = Courses::::get(course_id).ok_or(Error::::CourseNotFound)?; ensure!(course.owner == caller, Error::::NotCourseOwner); // Get and validate enrollment let mut enrollment = Enrollments::::get((&student, course_id)).ok_or(Error::::NotEnrolled)?; ensure!(enrollment.completed_at.is_none(), Error::::CourseAlreadyCompleted); // Mark completion enrollment.completed_at = Some(pezframe_system::Pezpallet::::block_number()); enrollment.points_earned = points; Enrollments::::insert((&student, course_id), enrollment); Self::deposit_event(Event::CourseCompleted { student, course_id, points }); Ok(()) } #[pezpallet::call_index(3)] #[pezpallet::weight(T::WeightInfo::archive_course())] pub fn archive_course(origin: OriginFor, course_id: u32) -> DispatchResult { let caller = T::AdminOrigin::ensure_origin(origin)?; let mut course = Courses::::get(course_id).ok_or(Error::::CourseNotFound)?; ensure!(course.owner == caller, Error::::NotCourseOwner); course.status = CourseStatus::Archived; Courses::::insert(course_id, course); Self::deposit_event(Event::CourseArchived { course_id }); Ok(()) } } impl Pezpallet { pub fn get_perwerde_score(who: &T::AccountId) -> u32 { StudentCourses::::get(who) .iter() .filter_map(|course_id| Enrollments::::get((who, *course_id))) .filter(|enrollment| enrollment.completed_at.is_some()) .map(|enrollment| enrollment.points_earned) .sum() } } }