# -*- 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': '

Testing invalid date

', 'date_published': today, 'expiry_date': today - timedelta(days=2), 'state': 'draft', }) # Valid creation ann = self.env['education.announcement'].create({ 'title': 'Orientation Notice', 'content': '

Welcome all new students!

', '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': '

Campus closed tomorrow

', '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': '

Mandatory for Program A

', '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': '

Exclusive for Program B

', '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': '

Section 2 only

', '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': '

Active

', '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': '

Not yet active

', 'date_published': today + timedelta(days=2), 'state': 'published', }) # Expired notice ann_expired = self.env['education.announcement'].create({ 'title': 'Expired Notice', 'content': '

Expired yesterday

', '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': '

Registrations open

', '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': '

Administrative text

', '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': '

Hacked

', '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()