itsulu-blog-publisher/addons/itsulu_blog_publisher/views/website_blog_publisher_templates.xml
Nicholas Riegel ec1a267ead test: RED phase — first test passing for blog.topic model
Setup:
- K8s test job with init container auto-installing itsulu_blog_publisher
- Dockerfile simplified: symlink addon to /var/lib/odoo/addons, no conftest needed
- Postgres init container creates fresh test DB for each job

Fixes:
- Disabled website_blog_publisher_templates.xml (RELAXNG validation issue in Odoo 17)
  Template elements need schema rework; deferred to Phase 2.5
- Fixed XML entity escaping in retained template code (&& → &&)

Test Result:
 TestBlogTopicQueueManagement::test_topic_is_created_with_pending_state PASSED
   Model itsulu.blog.topic registers correctly
   Default state='pending' works as expected

Next:
- Run all 7 blog_topic tests to ensure complete coverage
- GREEN phase: implement remaining model methods/fields
- REFACTOR: pre-commit check

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-29 22:50:10 -04:00

227 lines
12 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ================================================================
Website Toolbar "Generate New Post" Button
Visible only to website admins on the /blog listing and any
blog post page. Clicking opens the Generate Now wizard via
a JSON-RPC call, then redirects to the new post.
================================================================ -->
<template id="website_blog_publisher_button"
name="Blog Publisher — Generate Button"
inherit_id="website_blog.blog_post_short"
active="True">
<xpath expr="//div[hasclass('o_website_top_actions')]" position="inside">
<t t-if="request.env.user.has_group('website.group_website_designer')">
<a href="/blog-publisher/generate"
class="btn btn-primary btn-sm ms-2"
data-test-id="website-btn-generate-post">
✨ Generate New Post
</a>
</t>
</xpath>
</template>
<!-- Also inject on the /blog index page header -->
<template id="website_blog_publisher_button_index"
name="Blog Publisher — Generate Button (Blog Index)"
inherit_id="website_blog.blogs"
active="True">
<xpath expr="//div[hasclass('container')]" position="before">
<t t-if="request.env.user.has_group('website.group_website_designer')">
<div class="o_blog_publisher_toolbar text-end mb-3">
<a href="/blog-publisher/generate"
class="btn btn-primary"
data-test-id="website-btn-generate-post-index">
✨ Generate New Post
</a>
</div>
</t>
</xpath>
</template>
<!-- ================================================================
Website Generate Wizard Page (/blog-publisher/generate)
A lightweight website page that renders the wizard fields and
submits via JSON-RPC. After success, redirects to the new post.
================================================================ -->
<template id="website_blog_publisher_generate_page"
name="Blog Publisher — Generate Post Page"
page="True">
<t t-call="website.layout">
<t t-set="title">Generate Blog Post</t>
<div class="container mt-5 mb-5" style="max-width: 700px;">
<h2>✨ Generate New Blog Post</h2>
<p class="text-muted">
One click creates a fully-written post with SEO fields and social media copy.
</p>
<div id="blog_publisher_form">
<div class="mb-3">
<label for="bp_topic" class="form-label fw-bold">Topic</label>
<input type="text" id="bp_topic" name="topic"
class="form-control"
data-test-id="website-topic-input"
placeholder="Leave blank to use queue or let AI choose"/>
<div class="form-text">
If blank, the next topic from your queue will be used,
or the AI will choose a topic relevant to ITSulu's services.
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="bp_blog" class="form-label fw-bold">Target Blog</label>
<select id="bp_blog" name="blog_id" class="form-select"
data-test-id="website-blog-select">
<t t-foreach="blogs" t-as="blog">
<option t-att-value="blog.id"
t-att-selected="blog.id == default_blog_id">
<t t-esc="blog.name"/>
</option>
</t>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="bp_provider" class="form-label fw-bold">LLM Provider</label>
<select id="bp_provider" name="llm_provider" class="form-select"
data-test-id="website-provider-select">
<option value="anthropic">Anthropic Claude</option>
<option value="openai">OpenAI ChatGPT</option>
<option value="gemini">Google Gemini</option>
<option value="ollama">Ollama / Open WebUI</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="bp_model" class="form-label fw-bold">Model</label>
<input type="text" id="bp_model" name="llm_model"
class="form-control"
data-test-id="website-model-input"
t-att-value="default_model or 'claude-sonnet-4-20250514'"/>
</div>
<div class="col-md-6 mb-3">
<label for="bp_image" class="form-label fw-bold">Image Provider</label>
<select id="bp_image" name="image_provider" class="form-select">
<option value="none">No Image</option>
<option value="openai_dalle">OpenAI DALL·E 3</option>
<option value="google_imagen">Google Imagen</option>
<option value="stable_diff">Stable Diffusion (Ollama)</option>
</select>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="bp_publish"
name="auto_publish" value="1" checked="checked"
data-test-id="website-auto-publish"/>
<label class="form-check-label fw-bold" for="bp_publish">
Publish Immediately
</label>
</div>
</div>
<div class="mb-4">
<p class="fw-bold mb-2">Social Media Platforms:</p>
<div class="row">
<div class="col-6 col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="bp_twitter" name="platform_twitter" value="1" checked="checked"/>
<label class="form-check-label" for="bp_twitter">🐦 X/Twitter</label>
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="bp_bluesky" name="platform_bluesky" value="1" checked="checked"/>
<label class="form-check-label" for="bp_bluesky">🌐 BlueSky</label>
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="bp_mastodon" name="platform_mastodon" value="1" checked="checked"/>
<label class="form-check-label" for="bp_mastodon">🦣 Mastodon</label>
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-check">
<input class="form-check-input" type="checkbox"
id="bp_linkedin" name="platform_linkedin" value="1" checked="checked"/>
<label class="form-check-label" for="bp_linkedin">💼 LinkedIn</label>
</div>
</div>
</div>
</div>
<div id="bp_status" class="alert d-none" role="alert"></div>
<button id="bp_submit" type="button" class="btn btn-primary btn-lg"
data-test-id="website-btn-submit-generate">
✨ Generate Post
</button>
<a href="/blog" class="btn btn-outline-secondary btn-lg ms-2">Cancel</a>
</div>
<script type="text/javascript">
document.getElementById('bp_submit').addEventListener('click', function() {
var btn = this;
var status = document.getElementById('bp_status');
btn.disabled = true;
btn.textContent = '⏳ Generating... (this takes 20-60 seconds)';
status.className = 'alert alert-info';
status.textContent = 'Calling the AI — please wait...';
status.classList.remove('d-none');
var data = {
jsonrpc: '2.0',
method: 'call',
params: {
topic: document.getElementById('bp_topic').value,
blog_id: parseInt(document.getElementById('bp_blog').value),
llm_provider: document.getElementById('bp_provider').value,
llm_model: document.getElementById('bp_model').value,
image_provider: document.getElementById('bp_image').value,
auto_publish: document.getElementById('bp_publish').checked,
platform_twitter: document.getElementById('bp_twitter').checked,
platform_bluesky: document.getElementById('bp_bluesky').checked,
platform_mastodon: document.getElementById('bp_mastodon').checked,
platform_linkedin: document.getElementById('bp_linkedin').checked,
}
};
fetch('/blog-publisher/generate/submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(function(r) { return r.json(); })
.then(function(result) {
if (result.result &amp;&amp; result.result.redirect_url) {
window.location.href = result.result.redirect_url;
} else if (result.error) {
status.className = 'alert alert-danger';
status.textContent = 'Error: ' + (result.error.data.message || result.error.message);
btn.disabled = false;
btn.textContent = '✨ Generate Post';
}
})
.catch(function(err) {
status.className = 'alert alert-danger';
status.textContent = 'Network error: ' + err.message;
btn.disabled = false;
btn.textContent = '✨ Generate Post';
});
});
</script>
</div>
</t>
</template>
</odoo>