Demo data - Quick demo creating - pending few works on this
This commit is contained in:
parent
a3fcd51571
commit
0de280b2e7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
addons/education_base/tests/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/education_base/tests/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -224,6 +224,15 @@ class EducationDashboard extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
openDepartments() {
|
||||
this.actionService.doAction({
|
||||
name: "Departments", type: "ir.actions.act_window",
|
||||
res_model: "education.department",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
target: "current"
|
||||
});
|
||||
}
|
||||
|
||||
openAddStudent() {
|
||||
this.actionService.doAction({
|
||||
name: "New Student", type: "ir.actions.act_window",
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Programs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edu-kpi">
|
||||
<div class="edu-kpi" t-on-click="openDepartments">
|
||||
<div class="edu-icon" style="background:#fff7ed;color:#ea580c;"><i class="fa fa-sitemap"/></div>
|
||||
<div>
|
||||
<div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.dept_count || 0"/></div>
|
||||
|
||||
14
addons/education_demo_data/__init__.py
Normal file
14
addons/education_demo_data/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import controllers
|
||||
|
||||
def post_init_hook(env):
|
||||
"""
|
||||
Hook to run after module installation.
|
||||
Creates basic demo data if it doesn't exist.
|
||||
"""
|
||||
from .demo_data_generator import generate_demo_data
|
||||
try:
|
||||
generate_demo_data(env)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).error(f"Failed to generate demo data: {e}")
|
||||
24
addons/education_demo_data/__manifest__.py
Normal file
24
addons/education_demo_data/__manifest__.py
Normal file
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Education Demo Data',
|
||||
'version': '19.0.1.0.0',
|
||||
'summary': 'Demo Data & Secure Demo Login for Education ERP',
|
||||
'author': 'Metatroncube',
|
||||
'category': 'Education',
|
||||
'depends': [
|
||||
'education_base',
|
||||
'education_student',
|
||||
'education_lms',
|
||||
'education_portal',
|
||||
'education_exam',
|
||||
'education_timetable'
|
||||
],
|
||||
'data': [
|
||||
'data/demo_users.xml',
|
||||
],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'post_init_hook': 'post_init_hook',
|
||||
}
|
||||
BIN
addons/education_demo_data/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/education_demo_data/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
2
addons/education_demo_data/controllers/__init__.py
Normal file
2
addons/education_demo_data/controllers/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import demo_login
|
||||
Binary file not shown.
Binary file not shown.
79
addons/education_demo_data/controllers/demo_login.py
Normal file
79
addons/education_demo_data/controllers/demo_login.py
Normal file
@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo import http, exceptions
|
||||
# pyrefly: ignore [missing-import]
|
||||
from odoo.http import request
|
||||
|
||||
class EducationDemoController(http.Controller):
|
||||
|
||||
@http.route('/demo/login', type='http', auth='public', methods=['POST'], website=True, csrf=False)
|
||||
def demo_login(self, role=None, **kw):
|
||||
"""
|
||||
Secure Demo Login Endpoint.
|
||||
Accepts only 'role' (admin, faculty, student).
|
||||
Redirects to safe destinations.
|
||||
"""
|
||||
if not role:
|
||||
raise exceptions.AccessDenied("Missing role parameter.")
|
||||
|
||||
# Map allowed roles to specific, pre-configured demo user logins
|
||||
# Do not allow arbitrary user login
|
||||
demo_accounts = {
|
||||
'admin': {
|
||||
'login': 'demo.admin',
|
||||
'password': 'Demo@Admin2026',
|
||||
'redirect': '/web'
|
||||
},
|
||||
'faculty': {
|
||||
'login': 'demo.faculty',
|
||||
'password': 'Demo@Faculty2026',
|
||||
'redirect': '/web'
|
||||
},
|
||||
'student': {
|
||||
'login': 'demo.student',
|
||||
'password': 'Demo@Student2026',
|
||||
'redirect': '/my/education/dashboard'
|
||||
}
|
||||
}
|
||||
|
||||
if role not in demo_accounts:
|
||||
raise exceptions.AccessDenied("Invalid role.")
|
||||
|
||||
account = demo_accounts[role]
|
||||
db = request.db
|
||||
|
||||
if not db:
|
||||
# If no DB is set, force it to the single DB (we can get it if there's only one)
|
||||
dbs = http.db_list()
|
||||
if len(dbs) == 1:
|
||||
db = dbs[0]
|
||||
else:
|
||||
return request.redirect('/web/login')
|
||||
|
||||
try:
|
||||
credential = {
|
||||
'login': account['login'],
|
||||
'password': account['password'],
|
||||
'type': 'password'
|
||||
}
|
||||
if request.env.cr:
|
||||
auth_info = request.session.authenticate(request.env, credential)
|
||||
uid = auth_info.get('uid') if auth_info else request.session.uid
|
||||
else:
|
||||
import odoo
|
||||
with odoo.modules.registry.Registry(db).cursor() as cr:
|
||||
env = odoo.api.Environment(cr, None, {})
|
||||
auth_info = request.session.authenticate(env, credential)
|
||||
request._save_session(env)
|
||||
request.session.db = db
|
||||
uid = auth_info.get('uid') if auth_info else request.session.uid
|
||||
except Exception:
|
||||
uid = False
|
||||
|
||||
if not uid:
|
||||
# Maybe the demo user doesn't exist yet, or password was changed.
|
||||
# Redirect to login with error.
|
||||
return request.redirect('/web/login?error=Demo account not properly configured.')
|
||||
|
||||
# Protect against open redirects: Only use our pre-defined safe redirects
|
||||
return request.redirect(account['redirect'])
|
||||
27
addons/education_demo_data/data/demo_users.xml
Normal file
27
addons/education_demo_data/data/demo_users.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<!-- Demo Admin User -->
|
||||
<record id="demo_user_admin" model="res.users">
|
||||
<field name="name">Demo Admin</field>
|
||||
<field name="login">demo.admin</field>
|
||||
<field name="password">Demo@Admin2026</field>
|
||||
<field name="email">demo.admin@example.com</field>
|
||||
</record>
|
||||
|
||||
<!-- Demo Faculty User -->
|
||||
<record id="demo_user_faculty" model="res.users">
|
||||
<field name="name">Demo Faculty</field>
|
||||
<field name="login">demo.faculty</field>
|
||||
<field name="password">Demo@Faculty2026</field>
|
||||
<field name="email">demo.faculty@example.com</field>
|
||||
</record>
|
||||
|
||||
<!-- Demo Student Portal User -->
|
||||
<!-- Usually students are portal users -->
|
||||
<record id="demo_user_student" model="res.users">
|
||||
<field name="name">Demo Student</field>
|
||||
<field name="login">demo.student</field>
|
||||
<field name="password">Demo@Student2026</field>
|
||||
<field name="email">demo.student@example.com</field>
|
||||
</record>
|
||||
</odoo>
|
||||
224
addons/education_demo_data/demo_data_generator.py
Normal file
224
addons/education_demo_data/demo_data_generator.py
Normal file
@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
def generate_demo_data(env):
|
||||
"""
|
||||
Generate realistic demo data for the Education ERP.
|
||||
We do this via ORM to avoid complex XML constraint failures.
|
||||
"""
|
||||
# 1. Company / Campus / Department
|
||||
demo_company = env['res.company'].search([('name', '=', 'Metatron Demo Company')], limit=1)
|
||||
if not demo_company:
|
||||
demo_company = env['res.company'].create({
|
||||
'name': 'Metatron Demo Company',
|
||||
'email': 'demo@metatroncubesolutions.com'
|
||||
})
|
||||
# Explicitly ensure the main admin (ID 2) does not have access to this company
|
||||
main_admin = env['res.users'].browse(2)
|
||||
if main_admin.exists():
|
||||
main_admin.sudo().write({'company_ids': [(3, demo_company.id)]})
|
||||
|
||||
company = demo_company
|
||||
|
||||
campus = env['education.campus'].search([('name', '=', 'Demo Campus')], limit=1)
|
||||
if not campus:
|
||||
campus = env['education.campus'].create({
|
||||
'name': 'Demo Campus',
|
||||
'code': 'DEMO-CAMPUS',
|
||||
'company_id': company.id
|
||||
})
|
||||
|
||||
dept = env['education.department'].search([('name', '=', 'Computer Science Demo')], limit=1)
|
||||
if not dept:
|
||||
dept = env['education.department'].create({
|
||||
'name': 'Computer Science Demo',
|
||||
'code': 'CS-DEMO',
|
||||
'company_id': company.id,
|
||||
'campus_id': campus.id
|
||||
})
|
||||
|
||||
# 2. Academic Level & Program
|
||||
level = env['education.academic.level'].search([('name', '=', 'Undergraduate Demo')], limit=1)
|
||||
if not level:
|
||||
level = env['education.academic.level'].create({
|
||||
'name': 'Undergraduate Demo',
|
||||
'code': 'UG-DEMO',
|
||||
'company_id': company.id
|
||||
})
|
||||
|
||||
program = env['education.program'].search([('code', '=', 'BTECH-CS-DEMO')], limit=1)
|
||||
if not program:
|
||||
program = env['education.program'].create({
|
||||
'name': 'B.Tech Computer Science (Demo)',
|
||||
'code': 'BTECH-CS-DEMO',
|
||||
'level_id': level.id,
|
||||
'department_id': dept.id,
|
||||
'duration': 4,
|
||||
'duration_unit': 'years'
|
||||
})
|
||||
|
||||
# 3. Academic Year & Term
|
||||
import datetime
|
||||
today = datetime.date.today()
|
||||
|
||||
year = env['education.academic.year'].search([('name', '=', 'Demo Year 2026-2027')], limit=1)
|
||||
if not year:
|
||||
year = env['education.academic.year'].create({
|
||||
'name': 'Demo Year 2026-2027',
|
||||
'code': 'DY2627',
|
||||
'start_date': today,
|
||||
'end_date': today + datetime.timedelta(days=365),
|
||||
'company_id': company.id
|
||||
})
|
||||
|
||||
term = env['education.academic.term'].search([('code', '=', 'DT-FALL-26')], limit=1)
|
||||
if not term:
|
||||
term = env['education.academic.term'].create({
|
||||
'name': 'Demo Fall 2026',
|
||||
'code': 'DT-FALL-26',
|
||||
'academic_year_id': year.id,
|
||||
'start_date': today,
|
||||
'end_date': today + datetime.timedelta(days=120)
|
||||
})
|
||||
|
||||
# 4. Users & Roles
|
||||
admin_user = env['res.users'].search([('login', '=', 'demo.admin')], limit=1)
|
||||
faculty_user = env['res.users'].search([('login', '=', 'demo.faculty')], limit=1)
|
||||
student_user = env['res.users'].search([('login', '=', 'demo.student')], limit=1)
|
||||
|
||||
# Assign groups dynamically
|
||||
erp_manager_group = env.ref('base.group_erp_manager', raise_if_not_found=False)
|
||||
system_group = env.ref('base.group_system', raise_if_not_found=False)
|
||||
user_group = env.ref('base.group_user', raise_if_not_found=False)
|
||||
portal_group = env.ref('base.group_portal', raise_if_not_found=False)
|
||||
|
||||
if admin_user:
|
||||
admin_groups = []
|
||||
if erp_manager_group: admin_groups.append(erp_manager_group.id)
|
||||
if system_group: admin_groups.append(system_group.id)
|
||||
if admin_groups:
|
||||
admin_user.sudo().write({
|
||||
'groups_id': [(4, g) for g in admin_groups],
|
||||
'company_id': company.id,
|
||||
'company_ids': [(6, 0, [company.id])]
|
||||
})
|
||||
|
||||
if faculty_user and user_group:
|
||||
faculty_groups = [user_group.id]
|
||||
|
||||
# Give basic student read access
|
||||
student_user_grp = env.ref('education_student.group_education_student_user', raise_if_not_found=False)
|
||||
if student_user_grp: faculty_groups.append(student_user_grp.id)
|
||||
|
||||
# Give LMS Faculty access
|
||||
lms_faculty_grp = env.ref('education_lms.group_education_lms_faculty', raise_if_not_found=False)
|
||||
if lms_faculty_grp: faculty_groups.append(lms_faculty_grp.id)
|
||||
|
||||
faculty_user.sudo().write({
|
||||
'groups_id': [(4, g) for g in faculty_groups],
|
||||
'company_id': company.id,
|
||||
'company_ids': [(6, 0, [company.id])]
|
||||
})
|
||||
|
||||
if student_user and portal_group:
|
||||
student_user.sudo().write({
|
||||
'groups_id': [(4, portal_group.id)],
|
||||
'company_id': company.id,
|
||||
'company_ids': [(6, 0, [company.id])]
|
||||
})
|
||||
|
||||
# 5. Create Student Record
|
||||
if student_user:
|
||||
student = env['education.student'].search([('partner_id', '=', student_user.partner_id.id)], limit=1)
|
||||
if not student:
|
||||
student = env['education.student'].create({
|
||||
'first_name': 'Demo',
|
||||
'last_name': 'Student',
|
||||
'partner_id': student_user.partner_id.id,
|
||||
'program_id': program.id,
|
||||
'campus_id': campus.id,
|
||||
'academic_year_id': year.id,
|
||||
'status': 'active'
|
||||
})
|
||||
|
||||
# Map student ID to portal user if required by ERP architecture
|
||||
student_user.partner_id.write({'is_student': True})
|
||||
|
||||
# 6. Create LMS Course with Faculty
|
||||
if faculty_user:
|
||||
course = env['education.lms.course'].search([('code', '=', 'CS101-DEMO')], limit=1)
|
||||
if not course:
|
||||
course = env['education.lms.course'].create({
|
||||
'name': 'Introduction to Computer Science (Demo)',
|
||||
'code': 'CS101-DEMO',
|
||||
'faculty_id': faculty_user.id,
|
||||
'program_id': program.id,
|
||||
'state': 'published',
|
||||
'description': 'A comprehensive demo course.'
|
||||
})
|
||||
|
||||
# Create a section and material
|
||||
section = env['education.lms.section'].create({
|
||||
'name': 'Week 1: Basics',
|
||||
'course_id': course.id,
|
||||
'sequence': 1
|
||||
})
|
||||
|
||||
env['education.lms.material'].create({
|
||||
'name': 'Syllabus & Introduction',
|
||||
'section_id': section.id,
|
||||
'material_type': 'document',
|
||||
'content': 'Welcome to the course!'
|
||||
})
|
||||
|
||||
# Create an assignment
|
||||
env['education.lms.assignment'].create({
|
||||
'name': 'First Demo Assignment',
|
||||
'course_id': course.id,
|
||||
'due_date': today + datetime.timedelta(days=7),
|
||||
'max_marks': 100,
|
||||
'state': 'published'
|
||||
})
|
||||
|
||||
# Enroll the demo student
|
||||
if student_user:
|
||||
student_record = env['education.student'].search([('partner_id', '=', student_user.partner_id.id)], limit=1)
|
||||
if student_record:
|
||||
env['education.lms.enrollment'].create({
|
||||
'course_id': course.id,
|
||||
'student_id': student_record.id,
|
||||
'state': 'enrolled'
|
||||
})
|
||||
|
||||
# 7. Library Demo Data (Optional, if library module exists)
|
||||
if 'education.library.book' in env:
|
||||
book = env['education.library.book'].search([('isbn', '=', 'DEMO-ISBN-123')], limit=1)
|
||||
if not book:
|
||||
book = env['education.library.book'].create({
|
||||
'name': 'The Art of Computer Programming (Demo Edition)',
|
||||
'author': 'Donald Knuth',
|
||||
'isbn': 'DEMO-ISBN-123',
|
||||
'total_copies': 5
|
||||
})
|
||||
|
||||
# If student is a library member, create a checkout
|
||||
if student_user:
|
||||
student_record = env['education.student'].search([('partner_id', '=', student_user.partner_id.id)], limit=1)
|
||||
if student_record:
|
||||
member = env['education.library.member'].search([('student_id', '=', student_record.id)], limit=1)
|
||||
if not member:
|
||||
member = env['education.library.member'].create({
|
||||
'student_id': student_record.id,
|
||||
'member_type': 'student',
|
||||
'state': 'active'
|
||||
})
|
||||
|
||||
env['education.library.checkout'].create({
|
||||
'member_id': member.id,
|
||||
'book_id': book.id,
|
||||
'checkout_date': today,
|
||||
'due_date': today + datetime.timedelta(days=14),
|
||||
'state': 'issued'
|
||||
})
|
||||
|
||||
env.cr.commit()
|
||||
|
||||
1
addons/education_demo_data/tests/__init__.py
Normal file
1
addons/education_demo_data/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_demo_login
|
||||
Binary file not shown.
Binary file not shown.
32
addons/education_demo_data/tests/test_demo_login.py
Normal file
32
addons/education_demo_data/tests/test_demo_login.py
Normal file
@ -0,0 +1,32 @@
|
||||
import odoo.tests
|
||||
from odoo.tests.common import HttpCase
|
||||
from unittest.mock import patch
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install')
|
||||
class TestDemoLogin(HttpCase):
|
||||
|
||||
def test_01_demo_login_admin(self):
|
||||
""" Test admin demo login via endpoint """
|
||||
response = self.url_open('/demo/login', data={'role': 'admin'})
|
||||
self.assertEqual(response.status_code, 200, "Should redirect or load a page")
|
||||
|
||||
def test_02_demo_login_faculty(self):
|
||||
""" Test faculty demo login """
|
||||
response = self.url_open('/demo/login', data={'role': 'faculty'})
|
||||
self.assertEqual(response.status_code, 200, "Should redirect or load a page")
|
||||
|
||||
def test_03_demo_login_student(self):
|
||||
""" Test student demo login """
|
||||
response = self.url_open('/demo/login', data={'role': 'student'})
|
||||
self.assertEqual(response.status_code, 200, "Should redirect or load a page")
|
||||
|
||||
def test_04_demo_login_invalid_role(self):
|
||||
""" Test invalid roles get rejected """
|
||||
response = self.url_open('/demo/login', data={'role': 'superadmin'})
|
||||
self.assertEqual(response.status_code, 403, "Should be Access Denied")
|
||||
|
||||
def test_05_demo_login_missing_role(self):
|
||||
""" Test missing role gets rejected """
|
||||
# using GET instead of POST causes a 405 Method Not Allowed, or missing role in POST causes 403
|
||||
response = self.url_open('/demo/login', data={})
|
||||
self.assertEqual(response.status_code, 403, "Should be Access Denied")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user