# -*- coding: utf-8 -*- from odoo.tests import tagged from odoo.tests.common import TransactionCase from hypothesis import given, strategies as st, settings, assume import statistics @tagged('post_install', '-at_install', 'helpdesk_rating_five_stars') class TestAverageCalculation(TransactionCase): """ Property-based tests for rating average calculation Requirements: 4.2 - Requirement 4.2: Calculate average ratings based on the 0-5 scale """ def setUp(self): super(TestAverageCalculation, self).setUp() self.Rating = self.env['rating.rating'] self.HelpdeskTeam = self.env['helpdesk.team'] self.HelpdeskTicket = self.env['helpdesk.ticket'] # Create a helpdesk team with rating enabled self.team = self.HelpdeskTeam.create({ 'name': 'Test Support Team', 'use_rating': True, }) def _create_ticket_with_ratings(self, rating_values): """ Helper method to create a ticket with multiple ratings Args: rating_values: List of rating values (1-5) Returns: tuple: (ticket, list of rating records) """ # Create a ticket ticket = self.HelpdeskTicket.create({ 'name': f'Test Ticket for ratings {rating_values}', 'team_id': self.team.id, }) # Create ratings for the ticket ratings = [] for rating_value in rating_values: rating = self.Rating.create({ 'res_model': 'helpdesk.ticket', 'res_id': ticket.id, 'rating': float(rating_value), 'consumed': True, }) ratings.append(rating) return ticket, ratings # Feature: helpdesk-rating-five-stars, Property 9: Average calculation uses correct scale @given(rating_values=st.lists( st.floats(min_value=1.0, max_value=5.0, allow_nan=False, allow_infinity=False), min_size=1, max_size=20 )) @settings(max_examples=100, deadline=None) def test_property_average_uses_correct_scale(self, rating_values): """ Property 9: Average calculation uses correct scale For any set of ratings, the calculated average should be based on the 0-5 scale. This property verifies that: 1. All individual ratings are in the 0-5 range 2. The calculated average is in the 0-5 range 3. The average matches the expected mathematical average of the input values Validates: Requirements 4.2 """ # Skip if we have no valid ratings assume(len(rating_values) > 0) # Create ticket with ratings ticket, ratings = self._create_ticket_with_ratings(rating_values) # Verify all individual ratings are in valid range (1-5) for rating in ratings: self.assertGreaterEqual(rating.rating, 1.0, f"Individual rating {rating.rating} should be >= 1.0") self.assertLessEqual(rating.rating, 5.0, f"Individual rating {rating.rating} should be <= 5.0") # Calculate expected average using Python's statistics module expected_avg = statistics.mean(rating_values) # Verify expected average is in valid range self.assertGreaterEqual(expected_avg, 1.0, f"Expected average {expected_avg} should be >= 1.0") self.assertLessEqual(expected_avg, 5.0, f"Expected average {expected_avg} should be <= 5.0") # Get the average from Odoo's rating system # Method 1: Use read_group to calculate average domain = [('res_model', '=', 'helpdesk.ticket'), ('res_id', '=', ticket.id)] result = self.Rating.read_group( domain=domain, fields=['rating:avg'], groupby=[] ) if result and result[0].get('rating'): calculated_avg = result[0]['rating'] # Verify calculated average is in valid range (1-5) self.assertGreaterEqual(calculated_avg, 1.0, f"Calculated average {calculated_avg} should be >= 1.0") self.assertLessEqual(calculated_avg, 5.0, f"Calculated average {calculated_avg} should be <= 5.0") # Verify calculated average matches expected average self.assertAlmostEqual(calculated_avg, expected_avg, places=2, msg=f"Calculated average {calculated_avg} should match expected {expected_avg}") def test_average_with_zero_ratings(self): """ Test that zero ratings (no rating) are handled correctly in average calculation Zero ratings should be excluded from average calculations as they represent "no rating" rather than a rating of 0 stars. Validates: Requirements 4.2 """ # Create ticket with mix of real ratings and zero ratings ticket = self.HelpdeskTicket.create({ 'name': 'Test Ticket with zero ratings', 'team_id': self.team.id, }) # Create some real ratings self.Rating.create({ 'res_model': 'helpdesk.ticket', 'res_id': ticket.id, 'rating': 5.0, 'consumed': True, }) self.Rating.create({ 'res_model': 'helpdesk.ticket', 'res_id': ticket.id, 'rating': 3.0, 'consumed': True, }) # Create a zero rating (no rating) self.Rating.create({ 'res_model': 'helpdesk.ticket', 'res_id': ticket.id, 'rating': 0.0, 'consumed': False, }) # Calculate average excluding zero ratings domain = [ ('res_model', '=', 'helpdesk.ticket'), ('res_id', '=', ticket.id), ('rating', '>', 0) # Exclude zero ratings ] result = self.Rating.read_group( domain=domain, fields=['rating:avg'], groupby=[] ) if result and result[0].get('rating'): calculated_avg = result[0]['rating'] expected_avg = (5.0 + 3.0) / 2 # Should be 4.0 # Verify average is calculated correctly without zero ratings self.assertAlmostEqual(calculated_avg, expected_avg, places=2, msg=f"Average should exclude zero ratings: {calculated_avg} vs {expected_avg}") def test_average_single_rating(self): """ Test that average calculation works correctly with a single rating Validates: Requirements 4.2 """ ticket, ratings = self._create_ticket_with_ratings([4.0]) domain = [('res_model', '=', 'helpdesk.ticket'), ('res_id', '=', ticket.id)] result = self.Rating.read_group( domain=domain, fields=['rating:avg'], groupby=[] ) if result and result[0].get('rating'): calculated_avg = result[0]['rating'] # Average of single rating should equal that rating self.assertAlmostEqual(calculated_avg, 4.0, places=2, msg="Average of single rating should equal the rating value") def test_average_all_same_ratings(self): """ Test that average calculation works correctly when all ratings are the same Validates: Requirements 4.2 """ ticket, ratings = self._create_ticket_with_ratings([3.0, 3.0, 3.0, 3.0]) domain = [('res_model', '=', 'helpdesk.ticket'), ('res_id', '=', ticket.id)] result = self.Rating.read_group( domain=domain, fields=['rating:avg'], groupby=[] ) if result and result[0].get('rating'): calculated_avg = result[0]['rating'] # Average of identical ratings should equal that rating self.assertAlmostEqual(calculated_avg, 3.0, places=2, msg="Average of identical ratings should equal the rating value") def test_average_extreme_values(self): """ Test that average calculation works correctly with extreme values (1 and 5) Validates: Requirements 4.2 """ ticket, ratings = self._create_ticket_with_ratings([1.0, 5.0]) domain = [('res_model', '=', 'helpdesk.ticket'), ('res_id', '=', ticket.id)] result = self.Rating.read_group( domain=domain, fields=['rating:avg'], groupby=[] ) if result and result[0].get('rating'): calculated_avg = result[0]['rating'] expected_avg = (1.0 + 5.0) / 2 # Should be 3.0 # Average of extremes should be midpoint self.assertAlmostEqual(calculated_avg, expected_avg, places=2, msg=f"Average of 1 and 5 should be 3.0: {calculated_avg}")