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
# 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 \
pytest \
pytest-odoo \
pytest-bdd \
pytest-cov \
pytest-html \
"pytest>=7,<8" \
"pytest-odoo<2" \
"pytest-bdd>=6,<7" \
"pytest-cov<5" \
"pytest-html<4" \
requests
# Copy addon to Odoo addons path

View file

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

View file

@ -16,11 +16,10 @@ from .factories import BlogPublisherFactory
class TestSEOPopulation(TransactionCase):
"""Verify that all SEO fields are correctly populated after blog post generation."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
cls.blog = cls.factory.blog(name='ITSulu Insights')
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
self.blog = self.factory.blog(name='ITSulu Insights')
def test_generated_post_has_non_empty_meta_title(self):
"""
@ -125,11 +124,10 @@ class TestSEOPopulation(TransactionCase):
class TestBlogPostSocialModel(TransactionCase):
"""Verify the itsulu.blog.post.social model stores all platform copy correctly."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
cls.blog = cls.factory.blog(name='ITSulu Insights')
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
self.blog = self.factory.blog(name='ITSulu Insights')
def test_social_record_is_linked_one_to_one_with_blog_post(self):
"""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.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
cls.blog = cls.factory.blog(name='ITSulu Insights')
cls.env['ir.config_parameter'].sudo().set_param(
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
self.blog = self.factory.blog(name='ITSulu Insights')
self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.notification_emails',
'nicholasr@itsulu.com',
)

View file

@ -15,11 +15,10 @@ from .factories import BlogPublisherFactory
class TestBlogScheduleConfiguration(TransactionCase):
"""Verify that schedule slot records are configured correctly."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
cls.blog = cls.factory.blog(name='ITSulu Insights')
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
self.blog = self.factory.blog(name='ITSulu Insights')
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.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
cls.blog = cls.factory.blog(name='ITSulu Insights')
cls.env['ir.config_parameter'].sudo().set_param(
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
self.blog = self.factory.blog(name='ITSulu Insights')
self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
)

View file

@ -15,10 +15,9 @@ from .factories import BlogPublisherFactory
class TestBlogTopicQueueManagement(TransactionCase):
"""Verify that the topic queue picks topics in priority order."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
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):
"""Verify that the LLM router dispatches to the correct backend provider."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
# 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'
)
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'
)
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'
)
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'
)
@ -170,11 +169,10 @@ class TestLLMRouterProviderDispatch(TransactionCase):
class TestLLMRouterTokenLogging(TransactionCase):
"""Verify that token usage is captured from provider responses."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = BlogPublisherFactory(cls.env)
cls.env['ir.config_parameter'].sudo().set_param(
def setUp(self):
super().setUp()
self.factory = BlogPublisherFactory(self.env)
self.env['ir.config_parameter'].sudo().set_param(
'itsulu_blog_publisher.anthropic_api_key', 'sk-ant-test-key'
)

View file

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

View file

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