port(14.0): fix web_ribbon view attr + convert tests to setUp/self.env

- views/blog_schedule_views.xml: web_ribbon invisible="active" (Odoo 17
  bare-expr syntax) -> attrs="{'invisible': [('active','=',True)]}".
  This was the view-validation error blocking module install on Odoo 14.
- tests: Odoo 14 TransactionCase exposes self.env in setUp(), not cls.env
  in setUpClass() (that pattern is Odoo 15+). Converted all 13 setUpClass
  blocks across 6 test files to setUp(self) + self.env/self.factory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nicholas Riegel 2026-05-30 11:44:24 -04:00
parent d0e974a25b
commit 6635313f58
8 changed files with 71 additions and 83 deletions

View file

@ -1,12 +1,14 @@
FROM odoo:14.0 FROM odoo:14.0
# Install Python testing dependencies using the system Python # Install Python testing dependencies. odoo:14.0 ships Python 3.7, so pin to
# the last releases that still support 3.7 (pytest 8 / pytest-bdd 7 / pytest-html 4
# all require 3.8+).
RUN python3 -m pip install --no-cache-dir \ RUN python3 -m pip install --no-cache-dir \
pytest \ "pytest>=7,<8" \
pytest-odoo \ "pytest-odoo<2" \
pytest-bdd \ "pytest-bdd>=6,<7" \
pytest-cov \ "pytest-cov<5" \
pytest-html \ "pytest-html<4" \
requests requests
# Copy addon to Odoo addons path # Copy addon to Odoo addons path

View file

@ -14,11 +14,10 @@ from .factories import BlogPublisherFactory
class TestBlogGenerationLogCreation(TransactionCase): class TestBlogGenerationLogCreation(TransactionCase):
"""Verify that generation log records capture the correct metadata.""" """Verify that generation log records capture the correct metadata."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights')
def test_successful_log_record_is_created_with_correct_fields(self): def test_successful_log_record_is_created_with_correct_fields(self):
""" """
@ -105,10 +104,9 @@ class TestBlogGenerationLogCreation(TransactionCase):
class TestBlogGenerationLogRetry(TransactionCase): class TestBlogGenerationLogRetry(TransactionCase):
"""Verify that failed logs expose a working Retry action.""" """Verify that failed logs expose a working Retry action."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env)
def test_error_log_action_retry_returns_wizard_action(self): def test_error_log_action_retry_returns_wizard_action(self):
""" """

View file

@ -16,11 +16,10 @@ from .factories import BlogPublisherFactory
class TestSEOPopulation(TransactionCase): class TestSEOPopulation(TransactionCase):
"""Verify that all SEO fields are correctly populated after blog post generation.""" """Verify that all SEO fields are correctly populated after blog post generation."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights')
def test_generated_post_has_non_empty_meta_title(self): def test_generated_post_has_non_empty_meta_title(self):
""" """
@ -125,11 +124,10 @@ class TestSEOPopulation(TransactionCase):
class TestBlogPostSocialModel(TransactionCase): class TestBlogPostSocialModel(TransactionCase):
"""Verify the itsulu.blog.post.social model stores all platform copy correctly.""" """Verify the itsulu.blog.post.social model stores all platform copy correctly."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights')
def test_social_record_is_linked_one_to_one_with_blog_post(self): def test_social_record_is_linked_one_to_one_with_blog_post(self):
"""Each blog post has at most one social record.""" """Each blog post has at most one social record."""
@ -200,12 +198,11 @@ class TestNotificationEmail(TransactionCase):
structure matching the [ITSulu Insights] template in the uploaded .eml. structure matching the [ITSulu Insights] template in the uploaded .eml.
""" """
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights') self.env['ir.config_parameter'].sudo().set_param(
cls.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.notification_emails', 'itsulu_blog_publisher.notification_emails',
'nicholasr@itsulu.com', 'nicholasr@itsulu.com',
) )

View file

@ -15,11 +15,10 @@ from .factories import BlogPublisherFactory
class TestBlogScheduleConfiguration(TransactionCase): class TestBlogScheduleConfiguration(TransactionCase):
"""Verify that schedule slot records are configured correctly.""" """Verify that schedule slot records are configured correctly."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights')
def test_schedule_slot_is_created_with_correct_defaults(self): def test_schedule_slot_is_created_with_correct_defaults(self):
""" """
@ -69,12 +68,11 @@ class TestBlogScheduleExecution(TransactionCase):
LLM calls are mocked we are testing orchestration, not the LLM. LLM calls are mocked we are testing orchestration, not the LLM.
""" """
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights') self.env['ir.config_parameter'].sudo().set_param(
cls.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
) )

View file

@ -15,10 +15,9 @@ from .factories import BlogPublisherFactory
class TestBlogTopicQueueManagement(TransactionCase): class TestBlogTopicQueueManagement(TransactionCase):
"""Verify that the topic queue picks topics in priority order.""" """Verify that the topic queue picks topics in priority order."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env)
def test_topic_is_created_with_pending_state(self): def test_topic_is_created_with_pending_state(self):
""" """

View file

@ -42,21 +42,20 @@ def _make_mock_llm_response(tokens_used=800):
class TestLLMRouterProviderDispatch(TransactionCase): class TestLLMRouterProviderDispatch(TransactionCase):
"""Verify that the LLM router dispatches to the correct backend provider.""" """Verify that the LLM router dispatches to the correct backend provider."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env)
# Store a known-valid stub API key in ir.config_parameter # Store a known-valid stub API key in ir.config_parameter
cls.env['ir.config_parameter'].sudo().set_param( self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
) )
cls.env['ir.config_parameter'].sudo().set_param( self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.openai_api_key', 'sk-openai-test-key' 'itsulu_blog_publisher.openai_api_key', 'sk-openai-test-key'
) )
cls.env['ir.config_parameter'].sudo().set_param( self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.gemini_api_key', 'gemini-test-key' 'itsulu_blog_publisher.gemini_api_key', 'gemini-test-key'
) )
cls.env['ir.config_parameter'].sudo().set_param( self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.ollama_base_url', 'http://localhost:11434' 'itsulu_blog_publisher.ollama_base_url', 'http://localhost:11434'
) )
@ -170,11 +169,10 @@ class TestLLMRouterProviderDispatch(TransactionCase):
class TestLLMRouterTokenLogging(TransactionCase): class TestLLMRouterTokenLogging(TransactionCase):
"""Verify that token usage is captured from provider responses.""" """Verify that token usage is captured from provider responses."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.env['ir.config_parameter'].sudo().set_param(
cls.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
) )

View file

@ -16,12 +16,11 @@ _logger = logging.getLogger(__name__)
class TestGenerationLatency(TransactionCase): class TestGenerationLatency(TransactionCase):
"""Measure time from run_generation() call to blog.post creation.""" """Measure time from run_generation() call to blog.post creation."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights') self.env['ir.config_parameter'].sudo().set_param(
cls.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
) )
@ -103,12 +102,11 @@ class TestGenerationLatency(TransactionCase):
class TestQueryCount(TransactionCase): class TestQueryCount(TransactionCase):
"""Verify N+1 query patterns don't exist in critical paths.""" """Verify N+1 query patterns don't exist in critical paths."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights') self.env['ir.config_parameter'].sudo().set_param(
cls.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
) )
@ -179,11 +177,10 @@ class TestQueryCount(TransactionCase):
class TestTokenUsageBaseline(TransactionCase): class TestTokenUsageBaseline(TransactionCase):
"""Establish token usage baseline for cost tracking.""" """Establish token usage baseline for cost tracking."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights')
def test_typical_post_uses_800_to_1200_tokens(self): def test_typical_post_uses_800_to_1200_tokens(self):
""" """
@ -231,12 +228,11 @@ class TestTokenUsageBaseline(TransactionCase):
class TestConcurrentGeneration(TransactionCase): class TestConcurrentGeneration(TransactionCase):
"""Test that concurrent post generation handles contention correctly.""" """Test that concurrent post generation handles contention correctly."""
@classmethod def setUp(self):
def setUpClass(cls): super().setUp()
super().setUpClass() self.factory = BlogPublisherFactory(self.env)
cls.factory = BlogPublisherFactory(cls.env) self.blog = self.factory.blog(name='ITSulu Insights')
cls.blog = cls.factory.blog(name='ITSulu Insights') self.env['ir.config_parameter'].sudo().set_param(
cls.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key' 'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
) )

View file

@ -38,7 +38,7 @@
</header> </header>
<sheet> <sheet>
<widget name="web_ribbon" title="Inactive" bg_color="bg-danger" <widget name="web_ribbon" title="Inactive" bg_color="bg-danger"
invisible="active"/> attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title"> <div class="oe_title">
<h1><field name="name" placeholder="e.g. Morning Post"/></h1> <h1><field name="name" placeholder="e.g. Morning Post"/></h1>
</div> </div>