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