assignment submission updated
This commit is contained in:
parent
b04e39b202
commit
40f4b9dc81
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Community OS - School',
|
'name': 'Community OS - School',
|
||||||
'version': '19.0.1.0.0',
|
'version': '19.0.1.1.0',
|
||||||
'category': 'Website/Education',
|
'category': 'Website/Education',
|
||||||
'summary': 'Programs, levels, classes, enrollment, attendance and parent portal for any weekend/community school',
|
'summary': 'Programs, levels, classes, enrollment, attendance and parent portal for any weekend/community school',
|
||||||
'description': """
|
'description': """
|
||||||
@ -35,9 +35,11 @@ with no code change.
|
|||||||
'data/mail_templates.xml',
|
'data/mail_templates.xml',
|
||||||
'data/ir_cron.xml',
|
'data/ir_cron.xml',
|
||||||
'views/school_views.xml',
|
'views/school_views.xml',
|
||||||
|
'views/school_assignment_views.xml',
|
||||||
'views/school_menus.xml',
|
'views/school_menus.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/portal_attendance_templates.xml',
|
'views/portal_attendance_templates.xml',
|
||||||
|
'views/portal_assignment_templates.xml',
|
||||||
'views/registration_templates.xml',
|
'views/registration_templates.xml',
|
||||||
],
|
],
|
||||||
'demo': [],
|
'demo': [],
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
from . import attendance
|
from . import attendance
|
||||||
from . import registration
|
from . import registration
|
||||||
|
from . import assignment
|
||||||
|
|||||||
244
addons/community_school/controllers/assignment.py
Normal file
244
addons/community_school/controllers/assignment.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import fields, http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_login(path):
|
||||||
|
"""Redirect to login if visitor is not authenticated."""
|
||||||
|
if request.env.user._is_public():
|
||||||
|
return request.redirect(f'/web/login?redirect={path}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SchoolAssignmentController(http.Controller):
|
||||||
|
|
||||||
|
def _get_student_for_user(self):
|
||||||
|
"""Return the community.school.student record linked to the current portal user."""
|
||||||
|
partner = request.env.user.partner_id
|
||||||
|
return request.env['community.school.student'].sudo().search(
|
||||||
|
[('partner_id', '=', partner.id)], limit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_teacher_classes(self):
|
||||||
|
partner = request.env.user.partner_id
|
||||||
|
return request.env['community.school.class'].sudo().search(
|
||||||
|
[('teacher_id', '=', partner.id)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# TEACHER: Create / manage assignments
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@http.route('/school/assignments/teacher', type='http', auth='public', website=True)
|
||||||
|
def teacher_assignment_list(self, **kwargs):
|
||||||
|
redirect = _require_login('/school/assignments/teacher')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
classes = self._get_teacher_classes()
|
||||||
|
if not classes:
|
||||||
|
return request.render('community_school.portal_no_classes', {})
|
||||||
|
assignments = request.env['community.school.assignment'].sudo().search(
|
||||||
|
[('class_id', 'in', classes.ids)], order='due_date asc'
|
||||||
|
)
|
||||||
|
return request.render('community_school.portal_teacher_assignment_list', {
|
||||||
|
'assignments': assignments,
|
||||||
|
'classes': classes,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route('/school/assignments/teacher/new', type='http', auth='public', website=True)
|
||||||
|
def teacher_assignment_new(self, **kwargs):
|
||||||
|
redirect = _require_login('/school/assignments/teacher/new')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
classes = self._get_teacher_classes()
|
||||||
|
if not classes:
|
||||||
|
return request.render('community_school.portal_no_classes', {})
|
||||||
|
|
||||||
|
if request.httprequest.method == 'POST':
|
||||||
|
class_id = int(kwargs.get('class_id', 0))
|
||||||
|
klass = classes.filtered(lambda c: c.id == class_id)
|
||||||
|
if not klass:
|
||||||
|
return request.redirect('/school/assignments/teacher')
|
||||||
|
vals = {
|
||||||
|
'name': kwargs.get('title', '').strip(),
|
||||||
|
'class_id': class_id,
|
||||||
|
'description': kwargs.get('description', '').strip() or False,
|
||||||
|
'due_date': kwargs.get('due_date') or False,
|
||||||
|
'max_score': int(kwargs.get('max_score') or 100),
|
||||||
|
'state': 'published',
|
||||||
|
}
|
||||||
|
request.env['community.school.assignment'].sudo().create(vals)
|
||||||
|
return request.redirect('/school/assignments/teacher')
|
||||||
|
|
||||||
|
return request.render('community_school.portal_teacher_assignment_form', {
|
||||||
|
'classes': classes,
|
||||||
|
'assignment': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route('/school/assignments/teacher/<int:assignment_id>/edit',
|
||||||
|
type='http', auth='public', website=True)
|
||||||
|
def teacher_assignment_edit(self, assignment_id, **kwargs):
|
||||||
|
redirect = _require_login(f'/school/assignments/teacher/{assignment_id}/edit')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
classes = self._get_teacher_classes()
|
||||||
|
assignment = request.env['community.school.assignment'].sudo().browse(assignment_id)
|
||||||
|
if not assignment.exists() or assignment.class_id not in classes:
|
||||||
|
return request.redirect('/school/assignments/teacher')
|
||||||
|
|
||||||
|
if request.httprequest.method == 'POST':
|
||||||
|
vals = {
|
||||||
|
'name': kwargs.get('title', '').strip(),
|
||||||
|
'class_id': int(kwargs.get('class_id', assignment.class_id.id)),
|
||||||
|
'description': kwargs.get('description', '').strip() or False,
|
||||||
|
'due_date': kwargs.get('due_date') or False,
|
||||||
|
'max_score': int(kwargs.get('max_score') or 100),
|
||||||
|
}
|
||||||
|
action = kwargs.get('action_state')
|
||||||
|
if action == 'publish':
|
||||||
|
vals['state'] = 'published'
|
||||||
|
elif action == 'close':
|
||||||
|
vals['state'] = 'closed'
|
||||||
|
elif action == 'draft':
|
||||||
|
vals['state'] = 'draft'
|
||||||
|
assignment.write(vals)
|
||||||
|
return request.redirect('/school/assignments/teacher')
|
||||||
|
|
||||||
|
return request.render('community_school.portal_teacher_assignment_form', {
|
||||||
|
'classes': classes,
|
||||||
|
'assignment': assignment,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route('/school/assignments/teacher/<int:assignment_id>/submissions',
|
||||||
|
type='http', auth='public', website=True)
|
||||||
|
def teacher_view_submissions(self, assignment_id, **kwargs):
|
||||||
|
redirect = _require_login(f'/school/assignments/teacher/{assignment_id}/submissions')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
classes = self._get_teacher_classes()
|
||||||
|
assignment = request.env['community.school.assignment'].sudo().browse(assignment_id)
|
||||||
|
if not assignment.exists() or assignment.class_id not in classes:
|
||||||
|
return request.redirect('/school/assignments/teacher')
|
||||||
|
|
||||||
|
submissions = request.env['community.school.submission'].sudo().search(
|
||||||
|
[('assignment_id', '=', assignment_id)]
|
||||||
|
)
|
||||||
|
return request.render('community_school.portal_teacher_submissions', {
|
||||||
|
'assignment': assignment,
|
||||||
|
'submissions': submissions,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route('/school/assignments/teacher/submission/<int:submission_id>/grade',
|
||||||
|
type='http', auth='public', website=True, methods=['POST'])
|
||||||
|
def teacher_grade_submission(self, submission_id, **kwargs):
|
||||||
|
redirect = _require_login(f'/school/assignments/teacher/submission/{submission_id}/grade')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
classes = self._get_teacher_classes()
|
||||||
|
submission = request.env['community.school.submission'].sudo().browse(submission_id)
|
||||||
|
if not submission.exists() or submission.assignment_id.class_id not in classes:
|
||||||
|
return request.redirect('/school/assignments/teacher')
|
||||||
|
|
||||||
|
score = kwargs.get('score', '').strip()
|
||||||
|
feedback = kwargs.get('feedback', '').strip()
|
||||||
|
vals = {'feedback': feedback or False, 'state': 'graded'}
|
||||||
|
if score.isdigit():
|
||||||
|
vals['score'] = int(score)
|
||||||
|
submission.write(vals)
|
||||||
|
return request.redirect(
|
||||||
|
f'/school/assignments/teacher/{submission.assignment_id.id}/submissions'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# STUDENT: View assignments & upload submissions
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@http.route('/school/assignments/student', type='http', auth='public', website=True)
|
||||||
|
def student_assignment_list(self, **kwargs):
|
||||||
|
redirect = _require_login('/school/assignments/student')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
student = self._get_student_for_user()
|
||||||
|
if not student:
|
||||||
|
return request.render('community_school.portal_no_student', {})
|
||||||
|
|
||||||
|
enrolled_class_ids = student.enrollment_ids.filtered(
|
||||||
|
lambda e: e.state == 'enrolled'
|
||||||
|
).mapped('class_id').ids
|
||||||
|
|
||||||
|
assignments = request.env['community.school.assignment'].sudo().search(
|
||||||
|
[('class_id', 'in', enrolled_class_ids), ('state', '=', 'published')],
|
||||||
|
order='due_date asc',
|
||||||
|
)
|
||||||
|
# Build map of existing submissions for this student
|
||||||
|
my_submissions = request.env['community.school.submission'].sudo().search(
|
||||||
|
[('student_id', '=', student.id), ('assignment_id', 'in', assignments.ids)]
|
||||||
|
)
|
||||||
|
submission_by_assignment = {s.assignment_id.id: s for s in my_submissions}
|
||||||
|
|
||||||
|
return request.render('community_school.portal_student_assignment_list', {
|
||||||
|
'assignments': assignments,
|
||||||
|
'submission_by_assignment': submission_by_assignment,
|
||||||
|
'student': student,
|
||||||
|
})
|
||||||
|
|
||||||
|
@http.route('/school/assignments/student/<int:assignment_id>',
|
||||||
|
type='http', auth='public', website=True)
|
||||||
|
def student_assignment_detail(self, assignment_id, **kwargs):
|
||||||
|
redirect = _require_login(f'/school/assignments/student/{assignment_id}')
|
||||||
|
if redirect:
|
||||||
|
return redirect
|
||||||
|
student = self._get_student_for_user()
|
||||||
|
if not student:
|
||||||
|
return request.render('community_school.portal_no_student', {})
|
||||||
|
|
||||||
|
assignment = request.env['community.school.assignment'].sudo().browse(assignment_id)
|
||||||
|
if not assignment.exists() or assignment.state != 'published':
|
||||||
|
return request.redirect('/school/assignments/student')
|
||||||
|
|
||||||
|
# Verify student is enrolled in this class
|
||||||
|
enrollment = student.enrollment_ids.filtered(
|
||||||
|
lambda e: e.class_id.id == assignment.class_id.id and e.state == 'enrolled'
|
||||||
|
)
|
||||||
|
if not enrollment:
|
||||||
|
return request.redirect('/school/assignments/student')
|
||||||
|
|
||||||
|
existing_submission = request.env['community.school.submission'].sudo().search(
|
||||||
|
[('assignment_id', '=', assignment_id), ('student_id', '=', student.id)], limit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.httprequest.method == 'POST':
|
||||||
|
if existing_submission:
|
||||||
|
return request.redirect(f'/school/assignments/student/{assignment_id}')
|
||||||
|
|
||||||
|
notes = kwargs.get('notes', '').strip()
|
||||||
|
file_upload = request.httprequest.files.get('submission_file')
|
||||||
|
attachment_data = False
|
||||||
|
attachment_name = False
|
||||||
|
if file_upload and file_upload.filename:
|
||||||
|
try:
|
||||||
|
attachment_data = base64.b64encode(file_upload.read())
|
||||||
|
attachment_name = file_upload.filename
|
||||||
|
except Exception:
|
||||||
|
_logger.warning('Failed to read uploaded submission file')
|
||||||
|
|
||||||
|
request.env['community.school.submission'].sudo().create({
|
||||||
|
'assignment_id': assignment_id,
|
||||||
|
'enrollment_id': enrollment[0].id,
|
||||||
|
'notes': notes or False,
|
||||||
|
'attachment_data': attachment_data,
|
||||||
|
'attachment_name': attachment_name,
|
||||||
|
'submitted_on': fields.Datetime.now(),
|
||||||
|
'state': 'submitted',
|
||||||
|
})
|
||||||
|
return request.redirect(f'/school/assignments/student/{assignment_id}')
|
||||||
|
|
||||||
|
return request.render('community_school.portal_student_assignment_detail', {
|
||||||
|
'assignment': assignment,
|
||||||
|
'submission': existing_submission,
|
||||||
|
'student': student,
|
||||||
|
})
|
||||||
@ -5,4 +5,5 @@ from . import school_class
|
|||||||
from . import school_student
|
from . import school_student
|
||||||
from . import school_enrollment
|
from . import school_enrollment
|
||||||
from . import school_attendance
|
from . import school_attendance
|
||||||
|
from . import school_assignment
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
|
|||||||
113
addons/community_school/models/school_assignment.py
Normal file
113
addons/community_school/models/school_assignment.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class CommunitySchoolAssignment(models.Model):
|
||||||
|
_name = 'community.school.assignment'
|
||||||
|
_description = 'School Assignment'
|
||||||
|
_order = 'due_date asc, id desc'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
|
||||||
|
name = fields.Char(string='Title', required=True, tracking=True)
|
||||||
|
class_id = fields.Many2one(
|
||||||
|
'community.school.class', string='Class', required=True, ondelete='cascade',
|
||||||
|
)
|
||||||
|
term_id = fields.Many2one(related='class_id.term_id', store=True)
|
||||||
|
teacher_id = fields.Many2one(related='class_id.teacher_id', store=True)
|
||||||
|
description = fields.Html(string='Instructions')
|
||||||
|
due_date = fields.Datetime(string='Due Date', required=True, tracking=True)
|
||||||
|
max_score = fields.Integer(string='Max Score', default=100)
|
||||||
|
state = fields.Selection(
|
||||||
|
[('draft', 'Draft'), ('published', 'Published'), ('closed', 'Closed')],
|
||||||
|
default='draft', required=True, tracking=True,
|
||||||
|
)
|
||||||
|
submission_ids = fields.One2many('community.school.submission', 'assignment_id')
|
||||||
|
submission_count = fields.Integer(compute='_compute_submission_count', store=True)
|
||||||
|
|
||||||
|
@api.depends('submission_ids')
|
||||||
|
def _compute_submission_count(self):
|
||||||
|
for assignment in self:
|
||||||
|
assignment.submission_count = len(assignment.submission_ids)
|
||||||
|
|
||||||
|
def action_publish(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.state = 'published'
|
||||||
|
|
||||||
|
def action_close(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.state = 'closed'
|
||||||
|
|
||||||
|
def action_reset_draft(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.state = 'draft'
|
||||||
|
|
||||||
|
def action_view_submissions(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': f'Submissions – {self.name}',
|
||||||
|
'res_model': 'community.school.submission',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'domain': [('assignment_id', '=', self.id)],
|
||||||
|
'context': {'default_assignment_id': self.id},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CommunitySchoolSubmission(models.Model):
|
||||||
|
_name = 'community.school.submission'
|
||||||
|
_description = 'Assignment Submission'
|
||||||
|
_order = 'submitted_on desc, id desc'
|
||||||
|
|
||||||
|
assignment_id = fields.Many2one(
|
||||||
|
'community.school.assignment', required=True, ondelete='cascade',
|
||||||
|
)
|
||||||
|
enrollment_id = fields.Many2one(
|
||||||
|
'community.school.enrollment', string='Student Enrollment', required=True, ondelete='cascade',
|
||||||
|
)
|
||||||
|
student_id = fields.Many2one(related='enrollment_id.student_id', store=True)
|
||||||
|
class_id = fields.Many2one(related='assignment_id.class_id', store=True)
|
||||||
|
|
||||||
|
submitted_on = fields.Datetime(string='Submitted On', readonly=True)
|
||||||
|
notes = fields.Text(string='Student Notes')
|
||||||
|
|
||||||
|
# File attachment stored as binary
|
||||||
|
attachment_name = fields.Char(string='File Name')
|
||||||
|
attachment_data = fields.Binary(string='Uploaded File', attachment=True)
|
||||||
|
|
||||||
|
# Grading
|
||||||
|
score = fields.Integer(string='Score')
|
||||||
|
feedback = fields.Text(string='Teacher Feedback')
|
||||||
|
state = fields.Selection(
|
||||||
|
[('submitted', 'Submitted'), ('graded', 'Graded')],
|
||||||
|
default='submitted', required=True,
|
||||||
|
)
|
||||||
|
is_late = fields.Boolean(compute='_compute_is_late', store=True)
|
||||||
|
|
||||||
|
_assignment_enrollment_uniq = models.Constraint(
|
||||||
|
'unique(assignment_id, enrollment_id)',
|
||||||
|
'A submission for this assignment by this student already exists.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('submitted_on', 'assignment_id.due_date')
|
||||||
|
def _compute_is_late(self):
|
||||||
|
for sub in self:
|
||||||
|
if sub.submitted_on and sub.assignment_id.due_date:
|
||||||
|
sub.is_late = sub.submitted_on > sub.assignment_id.due_date
|
||||||
|
else:
|
||||||
|
sub.is_late = False
|
||||||
|
|
||||||
|
@api.constrains('score', 'assignment_id')
|
||||||
|
def _check_score(self):
|
||||||
|
for sub in self:
|
||||||
|
if sub.score < 0:
|
||||||
|
raise ValidationError('Score cannot be negative.')
|
||||||
|
if sub.assignment_id and sub.score > sub.assignment_id.max_score:
|
||||||
|
raise ValidationError(
|
||||||
|
f'Score ({sub.score}) cannot exceed max score ({sub.assignment_id.max_score}).'
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_grade(self):
|
||||||
|
for sub in self:
|
||||||
|
sub.state = 'graded'
|
||||||
@ -15,3 +15,14 @@ access_community_school_student_portal,community.school.student portal read,mode
|
|||||||
access_community_school_enrollment_portal,community.school.enrollment portal read,model_community_school_enrollment,base.group_portal,1,0,0,0
|
access_community_school_enrollment_portal,community.school.enrollment portal read,model_community_school_enrollment,base.group_portal,1,0,0,0
|
||||||
access_community_school_attendance_portal,community.school.attendance portal read,model_community_school_attendance,base.group_portal,1,0,0,0
|
access_community_school_attendance_portal,community.school.attendance portal read,model_community_school_attendance,base.group_portal,1,0,0,0
|
||||||
access_community_school_class_portal,community.school.class portal read,model_community_school_class,base.group_portal,1,0,0,0
|
access_community_school_class_portal,community.school.class portal read,model_community_school_class,base.group_portal,1,0,0,0
|
||||||
|
access_community_school_assignment_coordinator,community.school.assignment coordinator,model_community_school_assignment,group_school_coordinator,1,1,1,1
|
||||||
|
access_community_school_assignment_teacher,community.school.assignment teacher,model_community_school_assignment,group_school_teacher,1,1,1,0
|
||||||
|
access_community_school_assignment_portal,community.school.assignment portal read,model_community_school_assignment,base.group_portal,1,0,0,0
|
||||||
|
access_community_school_submission_coordinator,community.school.submission coordinator,model_community_school_submission,group_school_coordinator,1,1,1,1
|
||||||
|
access_community_school_submission_teacher,community.school.submission teacher,model_community_school_submission,group_school_teacher,1,1,1,0
|
||||||
|
access_community_school_submission_portal,community.school.submission portal,model_community_school_submission,base.group_portal,1,1,1,0
|
||||||
|
access_community_school_class_student,community.school.class student,model_community_school_class,group_school_student,1,0,0,0
|
||||||
|
access_community_school_student_student,community.school.student student,model_community_school_student,group_school_student,1,0,0,0
|
||||||
|
access_community_school_enrollment_student,community.school.enrollment student,model_community_school_enrollment,group_school_student,1,0,0,0
|
||||||
|
access_community_school_assignment_student,community.school.assignment student,model_community_school_assignment,group_school_student,1,0,0,0
|
||||||
|
access_community_school_submission_student,community.school.submission student,model_community_school_submission,group_school_student,1,1,1,0
|
||||||
|
|||||||
|
@ -23,4 +23,67 @@
|
|||||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
<field name="comment">Can take attendance for their own classes.</field>
|
<field name="comment">Can take attendance for their own classes.</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="group_school_student" model="res.groups">
|
||||||
|
<field name="name">Student</field>
|
||||||
|
<field name="privilege_id" ref="privilege_school"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="comment">Student access to view assignments and submit work.</field>
|
||||||
|
</record>
|
||||||
|
<!-- Record Rules -->
|
||||||
|
<record id="rule_class_teacher" model="ir.rule">
|
||||||
|
<field name="name">Teacher Classes</field>
|
||||||
|
<field name="model_id" ref="model_community_school_class"/>
|
||||||
|
<field name="domain_force">[('teacher_id.user_ids', 'in', [user.id])]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_teacher'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_assignment_teacher" model="ir.rule">
|
||||||
|
<field name="name">Teacher Assignments</field>
|
||||||
|
<field name="model_id" ref="model_community_school_assignment"/>
|
||||||
|
<field name="domain_force">[('teacher_id.user_ids', 'in', [user.id])]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_teacher'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_submission_teacher" model="ir.rule">
|
||||||
|
<field name="name">Teacher Submissions</field>
|
||||||
|
<field name="model_id" ref="model_community_school_submission"/>
|
||||||
|
<field name="domain_force">[('assignment_id.teacher_id.user_ids', 'in', [user.id])]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_teacher'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_class_student" model="ir.rule">
|
||||||
|
<field name="name">Student Classes</field>
|
||||||
|
<field name="model_id" ref="model_community_school_class"/>
|
||||||
|
<field name="domain_force">[('enrollment_ids.student_id.partner_id.user_ids', 'in', [user.id])]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_student'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_assignment_student" model="ir.rule">
|
||||||
|
<field name="name">Student Assignments</field>
|
||||||
|
<field name="model_id" ref="model_community_school_assignment"/>
|
||||||
|
<field name="domain_force">[('class_id.enrollment_ids.student_id.partner_id.user_ids', 'in', [user.id])]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_student'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_submission_student" model="ir.rule">
|
||||||
|
<field name="name">Student Submissions</field>
|
||||||
|
<field name="model_id" ref="model_community_school_submission"/>
|
||||||
|
<field name="domain_force">[('student_id.partner_id.user_ids', 'in', [user.id])]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_student'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_class_coordinator" model="ir.rule">
|
||||||
|
<field name="name">Coordinator Classes</field>
|
||||||
|
<field name="model_id" ref="model_community_school_class"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_coordinator'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_assignment_coordinator" model="ir.rule">
|
||||||
|
<field name="name">Coordinator Assignments</field>
|
||||||
|
<field name="model_id" ref="model_community_school_assignment"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_coordinator'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_submission_coordinator" model="ir.rule">
|
||||||
|
<field name="name">Coordinator Submissions</field>
|
||||||
|
<field name="model_id" ref="model_community_school_submission"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_school_coordinator'))]"/>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
437
addons/community_school/views/portal_assignment_templates.xml
Normal file
437
addons/community_school/views/portal_assignment_templates.xml
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
SHARED: No student record found
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
<template id="portal_no_student" name="School: No Student Profile">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div class="container" style="max-width: 520px; margin-top: 60px;">
|
||||||
|
<p class="alert alert-info">
|
||||||
|
No student profile is linked to your account. Please contact the school coordinator.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
TEACHER: Assignment List
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
<template id="portal_teacher_assignment_list" name="School: Teacher – My Assignments">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div class="container o_school_assignments" style="max-width: 900px; margin-top: 32px; margin-bottom: 80px;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0">My Assignments</h2>
|
||||||
|
<a href="/school/assignments/teacher/new" class="btn btn-primary">
|
||||||
|
<i class="fa fa-plus me-1"/> New Assignment
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-if="not assignments">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No assignments yet. Click <strong>New Assignment</strong> to create one.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-if="assignments">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Class</th>
|
||||||
|
<th>Due Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Submissions</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="assignments" t-as="asgn">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong t-out="asgn.name"/>
|
||||||
|
</td>
|
||||||
|
<td t-out="asgn.class_id.name"/>
|
||||||
|
<td t-out="asgn.due_date" t-options='{"widget": "datetime"}'/>
|
||||||
|
<td>
|
||||||
|
<t t-if="asgn.state == 'draft'">
|
||||||
|
<span class="badge bg-secondary">Draft</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="asgn.state == 'published'">
|
||||||
|
<span class="badge bg-success">Published</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="asgn.state == 'closed'">
|
||||||
|
<span class="badge bg-dark">Closed</span>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a t-attf-href="/school/assignments/teacher/#{asgn.id}/submissions"
|
||||||
|
class="badge bg-primary text-decoration-none">
|
||||||
|
<t t-out="asgn.submission_count"/> submitted
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a t-attf-href="/school/assignments/teacher/#{asgn.id}/edit"
|
||||||
|
class="btn btn-sm btn-outline-secondary">Edit</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
TEACHER: Create / Edit Assignment Form
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
<template id="portal_teacher_assignment_form" name="School: Teacher – Assignment Form">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div class="container o_school_assignment_form" style="max-width: 700px; margin-top: 32px; margin-bottom: 80px;">
|
||||||
|
<h2>
|
||||||
|
<t t-if="not assignment">New Assignment</t>
|
||||||
|
<t t-else="">Edit Assignment</t>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" class="mt-4">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="asgn_title">Title <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="asgn_title" name="title" required="required"
|
||||||
|
t-att-value="assignment.name if assignment else ''"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="asgn_class">Class <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-select" id="asgn_class" name="class_id" required="required">
|
||||||
|
<option value="">-- Select Class --</option>
|
||||||
|
<t t-foreach="classes" t-as="klass">
|
||||||
|
<option t-att-value="klass.id"
|
||||||
|
t-att-selected="'selected' if assignment and assignment.class_id.id == klass.id else None"
|
||||||
|
t-out="klass.name"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="asgn_due">Due Date & Time <span class="text-danger">*</span></label>
|
||||||
|
<input type="datetime-local" class="form-control" id="asgn_due" name="due_date" required="required"
|
||||||
|
t-att-value="assignment.due_date.strftime('%Y-%m-%dT%H:%M') if assignment and assignment.due_date else ''"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="asgn_score">Max Score</label>
|
||||||
|
<input type="number" class="form-control" id="asgn_score" name="max_score" min="1" max="1000"
|
||||||
|
t-att-value="assignment.max_score if assignment else 100"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold" for="asgn_desc">Instructions / Description</label>
|
||||||
|
<textarea class="form-control" id="asgn_desc" name="description" rows="6">
|
||||||
|
<t t-if="assignment and assignment.description" t-out="assignment.description"/>
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button type="submit" class="btn btn-primary" name="action_state" value="publish">
|
||||||
|
<t t-if="not assignment">Create & Publish</t>
|
||||||
|
<t t-else="">Save & Publish</t>
|
||||||
|
</button>
|
||||||
|
<t t-if="assignment">
|
||||||
|
<t t-if="assignment.state != 'draft'">
|
||||||
|
<button type="submit" class="btn btn-outline-secondary" name="action_state" value="draft">
|
||||||
|
Set to Draft
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
<t t-if="assignment.state != 'closed'">
|
||||||
|
<button type="submit" class="btn btn-outline-dark" name="action_state" value="close">
|
||||||
|
Close Assignment
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<a href="/school/assignments/teacher" class="btn btn-link text-muted">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
TEACHER: View Submissions for an Assignment
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
<template id="portal_teacher_submissions" name="School: Teacher – Submissions">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div class="container o_school_submissions" style="max-width: 950px; margin-top: 32px; margin-bottom: 80px;">
|
||||||
|
<a href="/school/assignments/teacher" class="btn btn-sm btn-outline-secondary mb-3">
|
||||||
|
← Back to Assignments
|
||||||
|
</a>
|
||||||
|
<h2 t-out="assignment.name"/>
|
||||||
|
<p class="text-muted mb-1">
|
||||||
|
Class: <strong t-out="assignment.class_id.name"/>
|
||||||
|
· Due: <span t-out="assignment.due_date" t-options='{"widget": "datetime"}'/>
|
||||||
|
· Max Score: <strong t-out="assignment.max_score"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<t t-if="not submissions">
|
||||||
|
<div class="alert alert-info mt-4">No submissions received yet.</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-if="submissions">
|
||||||
|
<div class="table-responsive mt-4">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Student</th>
|
||||||
|
<th>Submitted On</th>
|
||||||
|
<th>Late?</th>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Grade</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="submissions" t-as="sub">
|
||||||
|
<tr>
|
||||||
|
<td t-out="sub.student_id.partner_id.name"/>
|
||||||
|
<td t-out="sub.submitted_on" t-options='{"widget": "datetime"}'/>
|
||||||
|
<td>
|
||||||
|
<t t-if="sub.is_late">
|
||||||
|
<span class="badge bg-warning text-dark">Late</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-if="sub.attachment_name">
|
||||||
|
<a t-attf-href="/web/content/community.school.submission/#{sub.id}/attachment_data/#{sub.attachment_name}?download=true"
|
||||||
|
class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fa fa-download me-1"/>
|
||||||
|
<t t-out="sub.attachment_name"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
<t t-else=""><span class="text-muted">No file</span></t>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-if="sub.notes">
|
||||||
|
<small t-out="sub.notes"/>
|
||||||
|
</t>
|
||||||
|
<t t-else=""><span class="text-muted">—</span></t>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-if="sub.state == 'graded'">
|
||||||
|
<strong t-out="sub.score"/> / <t t-out="assignment.max_score"/>
|
||||||
|
</t>
|
||||||
|
<t t-else=""><span class="text-muted">—</span></t>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-if="sub.state == 'graded'">
|
||||||
|
<span class="badge bg-success">Graded</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="badge bg-secondary">Pending</span>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<!-- Inline grade form -->
|
||||||
|
<form t-attf-action="/school/assignments/teacher/submission/#{sub.id}/grade"
|
||||||
|
method="post" class="d-flex gap-1 align-items-center">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
<input type="number" name="score" class="form-control form-control-sm"
|
||||||
|
style="width: 64px;"
|
||||||
|
t-att-value="sub.score if sub.state == 'graded' else ''"
|
||||||
|
t-att-max="assignment.max_score" min="0" placeholder="Pts"/>
|
||||||
|
<input type="text" name="feedback" class="form-control form-control-sm"
|
||||||
|
style="width: 110px;"
|
||||||
|
t-att-value="sub.feedback or ''"
|
||||||
|
placeholder="Feedback"/>
|
||||||
|
<button type="submit" class="btn btn-sm btn-success">✓</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
STUDENT: Assignment List
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
<template id="portal_student_assignment_list" name="School: Student – Assignments">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div class="container o_school_student_assignments" style="max-width: 860px; margin-top: 32px; margin-bottom: 80px;">
|
||||||
|
<h2 class="mb-4">My Assignments</h2>
|
||||||
|
|
||||||
|
<t t-if="not assignments">
|
||||||
|
<div class="alert alert-info">No assignments have been published for your class yet.</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-if="assignments">
|
||||||
|
<div class="row g-3">
|
||||||
|
<t t-foreach="assignments" t-as="asgn">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<t t-set="my_sub" t-value="submission_by_assignment.get(asgn.id)"/>
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<h5 class="card-title mb-0" t-out="asgn.name"/>
|
||||||
|
<t t-if="my_sub and my_sub.state == 'graded'">
|
||||||
|
<span class="badge bg-success">Graded</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="my_sub">
|
||||||
|
<span class="badge bg-primary">Submitted</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="badge bg-warning text-dark">Pending</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-1">
|
||||||
|
Class: <strong t-out="asgn.class_id.name"/>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted small mb-2">
|
||||||
|
Due: <strong t-out="asgn.due_date" t-options='{"widget": "datetime"}'/>
|
||||||
|
</p>
|
||||||
|
<t t-if="my_sub and my_sub.state == 'graded'">
|
||||||
|
<p class="mb-2">
|
||||||
|
Score: <strong t-out="my_sub.score"/> / <t t-out="asgn.max_score"/>
|
||||||
|
</p>
|
||||||
|
<t t-if="my_sub.feedback">
|
||||||
|
<p class="text-muted small mb-2">
|
||||||
|
Feedback: <em t-out="my_sub.feedback"/>
|
||||||
|
</p>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-0 pt-0">
|
||||||
|
<a t-attf-href="/school/assignments/student/#{asgn.id}"
|
||||||
|
class="btn btn-sm btn-primary w-100">
|
||||||
|
<t t-if="not my_sub">View & Submit</t>
|
||||||
|
<t t-else="">View Submission</t>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
|
STUDENT: Assignment Detail & Upload Form
|
||||||
|
═══════════════════════════════════════════════════════════ -->
|
||||||
|
<template id="portal_student_assignment_detail" name="School: Student – Assignment Detail">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div class="container o_school_assignment_detail" style="max-width: 720px; margin-top: 32px; margin-bottom: 80px;">
|
||||||
|
<a href="/school/assignments/student" class="btn btn-sm btn-outline-secondary mb-3">
|
||||||
|
← Back to Assignments
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Assignment header -->
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 t-out="assignment.name"/>
|
||||||
|
<p class="text-muted mb-1">
|
||||||
|
Class: <strong t-out="assignment.class_id.name"/>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted mb-1">
|
||||||
|
Due: <strong t-out="assignment.due_date" t-options='{"widget": "datetime"}'/>
|
||||||
|
</p>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Max Score: <strong t-out="assignment.max_score"/>
|
||||||
|
</p>
|
||||||
|
<t t-if="assignment.description">
|
||||||
|
<hr/>
|
||||||
|
<div t-out="assignment.description"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Already submitted view -->
|
||||||
|
<t t-if="submission">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Your Submission</h5>
|
||||||
|
<p class="text-muted small">
|
||||||
|
Submitted on: <t t-out="submission.submitted_on" t-options='{"widget": "datetime"}'/>
|
||||||
|
<t t-if="submission.is_late">
|
||||||
|
<span class="badge bg-warning text-dark ms-2">Late</span>
|
||||||
|
</t>
|
||||||
|
</p>
|
||||||
|
<t t-if="submission.notes">
|
||||||
|
<p><strong>Your Notes:</strong><br/><t t-out="submission.notes"/></p>
|
||||||
|
</t>
|
||||||
|
<t t-if="submission.attachment_name">
|
||||||
|
<a t-attf-href="/web/content/community.school.submission/#{submission.id}/attachment_data/#{submission.attachment_name}?download=true"
|
||||||
|
class="btn btn-outline-primary">
|
||||||
|
<i class="fa fa-download me-1"/> Download: <t t-out="submission.attachment_name"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Graded result -->
|
||||||
|
<t t-if="submission.state == 'graded'">
|
||||||
|
<hr/>
|
||||||
|
<div class="alert alert-success mb-0">
|
||||||
|
<h6 class="alert-heading">Graded ✓</h6>
|
||||||
|
<p class="mb-1">
|
||||||
|
Score: <strong t-out="submission.score"/> / <t t-out="assignment.max_score"/>
|
||||||
|
</p>
|
||||||
|
<t t-if="submission.feedback">
|
||||||
|
<p class="mb-0">Feedback: <em t-out="submission.feedback"/></p>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="alert alert-info mt-3 mb-0">
|
||||||
|
Your submission is awaiting grading.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Submit form (shown only if not yet submitted) -->
|
||||||
|
<t t-if="not submission">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Submit Your Work</h5>
|
||||||
|
<form t-attf-action="/school/assignments/student/#{assignment.id}"
|
||||||
|
method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="sub_file">
|
||||||
|
Upload File <span class="text-muted fw-normal">(PDF, DOC, JPG, PNG, etc.)</span>
|
||||||
|
</label>
|
||||||
|
<input type="file" class="form-control" id="sub_file" name="submission_file"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold" for="sub_notes">Notes (optional)</label>
|
||||||
|
<textarea class="form-control" id="sub_notes" name="notes" rows="4"
|
||||||
|
placeholder="Add any notes or comments for your teacher…"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fa fa-upload me-1"/> Submit Assignment
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
155
addons/community_school/views/school_assignment_views.xml
Normal file
155
addons/community_school/views/school_assignment_views.xml
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ── Assignment list view ── -->
|
||||||
|
<record id="view_school_assignment_list" model="ir.ui.view">
|
||||||
|
<field name="name">community.school.assignment.list</field>
|
||||||
|
<field name="model">community.school.assignment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Assignments">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="class_id"/>
|
||||||
|
<field name="teacher_id"/>
|
||||||
|
<field name="due_date"/>
|
||||||
|
<field name="max_score"/>
|
||||||
|
<field name="submission_count" string="Submissions"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ── Assignment form view ── -->
|
||||||
|
<record id="view_school_assignment_form" model="ir.ui.view">
|
||||||
|
<field name="name">community.school.assignment.form</field>
|
||||||
|
<field name="model">community.school.assignment</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Assignment">
|
||||||
|
<header>
|
||||||
|
<button name="action_publish" string="Publish" type="object"
|
||||||
|
class="btn-primary" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_close" string="Close" type="object"
|
||||||
|
invisible="state != 'published'"/>
|
||||||
|
<button name="action_reset_draft" string="Reset to Draft" type="object"
|
||||||
|
invisible="state == 'draft'"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="draft,published,closed"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_view_submissions" type="object"
|
||||||
|
class="oe_stat_button" icon="fa-upload">
|
||||||
|
<field name="submission_count" widget="statinfo" string="Submissions"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name"/>
|
||||||
|
<h1><field name="name" placeholder="Assignment Title…"/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="class_id"/>
|
||||||
|
<field name="teacher_id" readonly="1"/>
|
||||||
|
<field name="term_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="due_date"/>
|
||||||
|
<field name="max_score"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Instructions" name="description">
|
||||||
|
<field name="description" widget="html" options="{'collaborative': True}"/>
|
||||||
|
</page>
|
||||||
|
<page string="Submissions" name="submissions">
|
||||||
|
<field name="submission_ids" mode="list" readonly="1">
|
||||||
|
<list>
|
||||||
|
<field name="student_id"/>
|
||||||
|
<field name="submitted_on"/>
|
||||||
|
<field name="is_late" string="Late?"/>
|
||||||
|
<field name="attachment_name" string="File"/>
|
||||||
|
<field name="score"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_school_assignment" model="ir.actions.act_window">
|
||||||
|
<field name="name">Assignments</field>
|
||||||
|
<field name="res_model">community.school.assignment</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ── Submission list view ── -->
|
||||||
|
<record id="view_school_submission_list" model="ir.ui.view">
|
||||||
|
<field name="name">community.school.submission.list</field>
|
||||||
|
<field name="model">community.school.submission</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Submissions">
|
||||||
|
<field name="assignment_id"/>
|
||||||
|
<field name="student_id"/>
|
||||||
|
<field name="class_id"/>
|
||||||
|
<field name="submitted_on"/>
|
||||||
|
<field name="is_late" string="Late?"/>
|
||||||
|
<field name="attachment_name" string="File"/>
|
||||||
|
<field name="score"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ── Submission form view ── -->
|
||||||
|
<record id="view_school_submission_form" model="ir.ui.view">
|
||||||
|
<field name="name">community.school.submission.form</field>
|
||||||
|
<field name="model">community.school.submission</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Submission">
|
||||||
|
<header>
|
||||||
|
<button name="action_grade" string="Mark as Graded" type="object"
|
||||||
|
class="btn-primary" invisible="state == 'graded'"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="submitted,graded"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group string="Assignment">
|
||||||
|
<field name="assignment_id"/>
|
||||||
|
<field name="class_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Student">
|
||||||
|
<field name="student_id"/>
|
||||||
|
<field name="enrollment_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group string="Submission">
|
||||||
|
<field name="submitted_on"/>
|
||||||
|
<field name="is_late" readonly="1"/>
|
||||||
|
<field name="attachment_name" string="File Name" readonly="1"/>
|
||||||
|
<field name="attachment_data" string="File" widget="binary"
|
||||||
|
filename="attachment_name"/>
|
||||||
|
</group>
|
||||||
|
<group string="Grading">
|
||||||
|
<field name="score"/>
|
||||||
|
<field name="feedback"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Student Notes">
|
||||||
|
<field name="notes" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_school_submission" model="ir.actions.act_window">
|
||||||
|
<field name="name">Submissions</field>
|
||||||
|
<field name="res_model">community.school.submission</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<menuitem id="menu_school_root" name="School" sequence="26"
|
<menuitem id="menu_school_root" name="School" sequence="26"
|
||||||
groups="community_school.group_school_coordinator,community_school.group_school_teacher"/>
|
groups="community_school.group_school_coordinator,community_school.group_school_teacher,community_school.group_school_student"/>
|
||||||
|
|
||||||
<menuitem id="menu_school_students" name="Students"
|
<menuitem id="menu_school_students" name="Students"
|
||||||
parent="menu_school_root" action="action_school_student" sequence="10"
|
parent="menu_school_root" action="action_school_student" sequence="10"
|
||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<menuitem id="menu_school_classes" name="Classes"
|
<menuitem id="menu_school_classes" name="Classes"
|
||||||
parent="menu_school_root" action="action_school_class" sequence="20"
|
parent="menu_school_root" action="action_school_class" sequence="20"
|
||||||
groups="community_school.group_school_coordinator"/>
|
groups="community_school.group_school_coordinator,community_school.group_school_teacher,community_school.group_school_student"/>
|
||||||
|
|
||||||
<menuitem id="menu_school_enrollments" name="Enrollments"
|
<menuitem id="menu_school_enrollments" name="Enrollments"
|
||||||
parent="menu_school_root" action="action_school_enrollment" sequence="30"
|
parent="menu_school_root" action="action_school_enrollment" sequence="30"
|
||||||
@ -23,6 +23,14 @@
|
|||||||
parent="menu_school_root" action="action_school_attendance_report" sequence="40"
|
parent="menu_school_root" action="action_school_attendance_report" sequence="40"
|
||||||
groups="community_school.group_school_coordinator"/>
|
groups="community_school.group_school_coordinator"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_school_assignments" name="Assignments"
|
||||||
|
parent="menu_school_root" action="action_school_assignment" sequence="50"
|
||||||
|
groups="community_school.group_school_coordinator,community_school.group_school_teacher,community_school.group_school_student"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_school_submissions" name="Submissions"
|
||||||
|
parent="menu_school_root" action="action_school_submission" sequence="55"
|
||||||
|
groups="community_school.group_school_coordinator,community_school.group_school_teacher,community_school.group_school_student"/>
|
||||||
|
|
||||||
<menuitem id="menu_school_configuration" name="Configuration"
|
<menuitem id="menu_school_configuration" name="Configuration"
|
||||||
parent="menu_school_root" sequence="90"
|
parent="menu_school_root" sequence="90"
|
||||||
groups="community_school.group_school_coordinator"/>
|
groups="community_school.group_school_coordinator"/>
|
||||||
|
|||||||
@ -228,4 +228,15 @@
|
|||||||
<field name="res_model">community.school.attendance</field>
|
<field name="res_model">community.school.attendance</field>
|
||||||
<field name="view_mode">pivot,list</field>
|
<field name="view_mode">pivot,list</field>
|
||||||
</record>
|
</record>
|
||||||
|
<!-- Partner (Teacher) -->
|
||||||
|
<record id="view_partner_form_school" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.form.school</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='category_id']" position="after">
|
||||||
|
<field name="is_teacher"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user