mirror of
https://gitlab.com/itsulu-odoo/itsulu-blog-publisher.git
synced 2026-05-30 23:41:23 +00:00
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>
227 lines
12 KiB
XML
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 && 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>
|