239 lines
10 KiB
Python
239 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
from datetime import date, timedelta
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.exceptions import ValidationError, AccessError
|
|
|
|
|
|
class TestEducationAnnouncement(TransactionCase):
|
|
|
|
def setUp(self):
|
|
super(TestEducationAnnouncement, self).setUp()
|
|
|
|
self.portal_group = self.env.ref('base.group_portal')
|
|
|
|
# Academic infrastructure
|
|
self.campus = self.env['education.campus'].create({'name': 'Ann Campus', 'code': 'AC'})
|
|
self.department = self.env['education.department'].create({'name': 'Ann Dept', 'code': 'AD', 'campus_id': self.campus.id})
|
|
self.level = self.env['education.academic.level'].create({'name': 'Ann Level', 'code': 'AL'})
|
|
self.program_a = self.env['education.program'].create({'name': 'Program A', 'code': 'PA', 'department_id': self.department.id, 'level_id': self.level.id})
|
|
self.program_b = self.env['education.program'].create({'name': 'Program B', 'code': 'PB', 'department_id': self.department.id, 'level_id': self.level.id})
|
|
self.year = self.env['education.academic.year'].create({'name': 'Ann Year', 'code': 'AY', 'start_date': '2026-01-01', 'end_date': '2026-12-31'})
|
|
self.term = self.env['education.academic.term'].create({'name': 'Ann Term', 'code': 'AT', 'academic_year_id': self.year.id, 'start_date': '2026-01-01', 'end_date': '2026-06-30'})
|
|
self.batch = self.env['education.batch'].create({'name': 'Ann Batch', 'code': 'AB'})
|
|
self.semester = self.env['education.semester'].create({'name': 'Ann Sem', 'code': 'AS'})
|
|
self.section_1 = self.env['education.section'].create({'name': 'Section 1', 'code': 'SEC1'})
|
|
self.section_2 = self.env['education.section'].create({'name': 'Section 2', 'code': 'SEC2'})
|
|
|
|
# Student User
|
|
self.user_student = self.env['res.users'].create({
|
|
'name': 'Ann Student',
|
|
'login': 'ann.student@test.com',
|
|
'email': 'ann.student@test.com',
|
|
'group_ids': [(6, 0, [self.portal_group.id])],
|
|
})
|
|
|
|
self.student = self.env['education.student'].create({
|
|
'first_name': 'Ann',
|
|
'last_name': 'Student',
|
|
'student_id': 'ANN-STU-01',
|
|
'admission_number': 'ANN-ADM-01',
|
|
'roll_number': 'ANN-RL-01',
|
|
'campus_id': self.campus.id,
|
|
'department_id': self.department.id,
|
|
'program_id': self.program_a.id,
|
|
'level_id': self.level.id,
|
|
'academic_year_id': self.year.id,
|
|
'academic_term_id': self.term.id,
|
|
'batch_id': self.batch.id,
|
|
'semester_id': self.semester.id,
|
|
'section_id': self.section_1.id,
|
|
'status': 'active',
|
|
'email': 'ann.student@test.com',
|
|
'portal_user_id': self.user_student.id,
|
|
})
|
|
|
|
def test_announcement_workflow_and_constraints(self):
|
|
"""Test draft, publish, archive states and date validation."""
|
|
today = date.today()
|
|
# Invalid expiry date constraint
|
|
with self.assertRaises(ValidationError):
|
|
self.env['education.announcement'].create({
|
|
'title': 'Invalid Date Announcement',
|
|
'content': '<p>Testing invalid date</p>',
|
|
'date_published': today,
|
|
'expiry_date': today - timedelta(days=2),
|
|
'state': 'draft',
|
|
})
|
|
|
|
# Valid creation
|
|
ann = self.env['education.announcement'].create({
|
|
'title': 'Orientation Notice',
|
|
'content': '<p>Welcome all new students!</p>',
|
|
'date_published': today,
|
|
'expiry_date': today + timedelta(days=10),
|
|
'announcement_type': 'notice',
|
|
'priority': '1',
|
|
'state': 'draft',
|
|
})
|
|
self.assertEqual(ann.state, 'draft')
|
|
self.assertFalse(ann.notification_sent)
|
|
|
|
# Publish
|
|
ann.action_publish()
|
|
self.assertEqual(ann.state, 'published')
|
|
self.assertTrue(ann.notification_sent)
|
|
self.assertIsNotNone(ann.notification_sent_date)
|
|
|
|
# Archive
|
|
ann.action_archive()
|
|
self.assertEqual(ann.state, 'archived')
|
|
|
|
# Reset to draft
|
|
ann.action_draft()
|
|
self.assertEqual(ann.state, 'draft')
|
|
|
|
def test_audience_filtering_all_vs_cohort(self):
|
|
"""Test that all-student and cohort-specific announcements filter accurately."""
|
|
today = date.today()
|
|
# Announcement for All Students
|
|
ann_all = self.env['education.announcement'].create({
|
|
'title': 'General Holiday Notice',
|
|
'content': '<p>Campus closed tomorrow</p>',
|
|
'date_published': today,
|
|
'audience_type': 'all',
|
|
'state': 'published',
|
|
})
|
|
|
|
# Cohort Announcement matching Student (Program A)
|
|
ann_prog_a = self.env['education.announcement'].create({
|
|
'title': 'Program A Seminar',
|
|
'content': '<p>Mandatory for Program A</p>',
|
|
'date_published': today,
|
|
'audience_type': 'cohort',
|
|
'program_id': self.program_a.id,
|
|
'state': 'published',
|
|
})
|
|
|
|
# Cohort Announcement for Program B (different program)
|
|
ann_prog_b = self.env['education.announcement'].create({
|
|
'title': 'Program B Workshop',
|
|
'content': '<p>Exclusive for Program B</p>',
|
|
'date_published': today,
|
|
'audience_type': 'cohort',
|
|
'program_id': self.program_b.id,
|
|
'state': 'published',
|
|
})
|
|
|
|
# Cohort Announcement for Section 2 (student is in Section 1)
|
|
ann_sec_2 = self.env['education.announcement'].create({
|
|
'title': 'Section 2 Lab Reschedule',
|
|
'content': '<p>Section 2 only</p>',
|
|
'date_published': today,
|
|
'audience_type': 'cohort',
|
|
'section_id': self.section_2.id,
|
|
'state': 'published',
|
|
})
|
|
|
|
# Check individual audience checks
|
|
self.assertTrue(ann_all.check_student_audience(self.student))
|
|
self.assertTrue(ann_prog_a.check_student_audience(self.student))
|
|
self.assertFalse(ann_prog_b.check_student_audience(self.student))
|
|
self.assertFalse(ann_sec_2.check_student_audience(self.student))
|
|
|
|
# Check get_announcements_for_student
|
|
records, total_count = self.env['education.announcement'].get_announcements_for_student(self.student)
|
|
record_ids = records.ids
|
|
self.assertIn(ann_all.id, record_ids)
|
|
self.assertIn(ann_prog_a.id, record_ids)
|
|
self.assertNotIn(ann_prog_b.id, record_ids)
|
|
self.assertNotIn(ann_sec_2.id, record_ids)
|
|
|
|
def test_publication_window_filtering(self):
|
|
"""Test that future-published and expired announcements are excluded."""
|
|
today = date.today()
|
|
|
|
# Currently active
|
|
ann_active = self.env['education.announcement'].create({
|
|
'title': 'Active Notice',
|
|
'content': '<p>Active</p>',
|
|
'date_published': today - timedelta(days=1),
|
|
'expiry_date': today + timedelta(days=5),
|
|
'state': 'published',
|
|
})
|
|
|
|
# Future notice (scheduled)
|
|
ann_future = self.env['education.announcement'].create({
|
|
'title': 'Future Notice',
|
|
'content': '<p>Not yet active</p>',
|
|
'date_published': today + timedelta(days=2),
|
|
'state': 'published',
|
|
})
|
|
|
|
# Expired notice
|
|
ann_expired = self.env['education.announcement'].create({
|
|
'title': 'Expired Notice',
|
|
'content': '<p>Expired yesterday</p>',
|
|
'date_published': today - timedelta(days=10),
|
|
'expiry_date': today - timedelta(days=1),
|
|
'state': 'published',
|
|
})
|
|
|
|
records, _ = self.env['education.announcement'].get_announcements_for_student(self.student)
|
|
record_ids = records.ids
|
|
self.assertIn(ann_active.id, record_ids)
|
|
self.assertNotIn(ann_future.id, record_ids)
|
|
self.assertNotIn(ann_expired.id, record_ids)
|
|
|
|
def test_notification_dispatch_and_duplicate_prevention(self):
|
|
"""Test that publishing sends notifications once and avoids duplicates unless forced."""
|
|
ann = self.env['education.announcement'].create({
|
|
'title': 'Annual Fest Announcement',
|
|
'content': '<p>Registrations open</p>',
|
|
'date_published': date.today(),
|
|
'audience_type': 'cohort',
|
|
'program_id': self.program_a.id,
|
|
'state': 'draft',
|
|
})
|
|
self.assertFalse(ann.notification_sent)
|
|
|
|
# First publish sends notification
|
|
ann.action_publish()
|
|
self.assertTrue(ann.notification_sent)
|
|
first_dispatch_time = ann.notification_sent_date
|
|
self.assertIsNotNone(first_dispatch_time)
|
|
self.assertGreaterEqual(ann.notification_recipient_count, 1)
|
|
|
|
# Calling _dispatch_notifications directly should NOT re-dispatch
|
|
ann._dispatch_notifications(force=False)
|
|
self.assertEqual(ann.notification_sent_date, first_dispatch_time)
|
|
|
|
# Forced resend should re-dispatch
|
|
ann.action_resend_notification()
|
|
self.assertTrue(ann.notification_sent)
|
|
|
|
def test_student_security_access_control(self):
|
|
"""Verify that portal student users cannot create, modify, or delete announcements."""
|
|
today = date.today()
|
|
ann = self.env['education.announcement'].create({
|
|
'title': 'Admin Announcement',
|
|
'content': '<p>Administrative text</p>',
|
|
'date_published': today,
|
|
'state': 'published',
|
|
})
|
|
|
|
# Student user attempt to create
|
|
with self.assertRaises(AccessError):
|
|
self.env['education.announcement'].with_user(self.user_student).create({
|
|
'title': 'Student Hack Announcement',
|
|
'content': '<p>Hacked</p>',
|
|
'date_published': today,
|
|
})
|
|
|
|
# Student user attempt to write
|
|
with self.assertRaises(AccessError):
|
|
ann.with_user(self.user_student).write({'title': 'Defaced Title'})
|
|
|
|
# Student user attempt to unlink
|
|
with self.assertRaises(AccessError):
|
|
ann.with_user(self.user_student).unlink()
|