open_blog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/app/assets/builds/open_blog/blog.css +2 -0
- data/app/assets/javascripts/open_blog/controllers/code_copy_controller.js +40 -0
- data/app/assets/javascripts/open_blog/controllers/reading_progress_controller.js +36 -0
- data/app/assets/javascripts/open_blog/controllers/search_controller.js +49 -0
- data/app/assets/javascripts/open_blog/controllers/share_controller.js +48 -0
- data/app/assets/javascripts/open_blog/controllers/theme_controller.js +51 -0
- data/app/assets/javascripts/open_blog/controllers/toc_controller.js +33 -0
- data/app/controllers/concerns/open_blog/reader_controller.rb +49 -0
- data/app/controllers/open_blog/api/v1/adoptions_controller.rb +27 -0
- data/app/controllers/open_blog/api/v1/approvals_controller.rb +15 -0
- data/app/controllers/open_blog/api/v1/authors_controller.rb +62 -0
- data/app/controllers/open_blog/api/v1/base_controller.rb +100 -0
- data/app/controllers/open_blog/api/v1/categories_controller.rb +33 -0
- data/app/controllers/open_blog/api/v1/connections_controller.rb +45 -0
- data/app/controllers/open_blog/api/v1/doctor_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/faq_extractions_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/findings_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/images_controller.rb +22 -0
- data/app/controllers/open_blog/api/v1/pages_controller.rb +67 -0
- data/app/controllers/open_blog/api/v1/posts_controller.rb +114 -0
- data/app/controllers/open_blog/api/v1/previews_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/records_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/redirects_controller.rb +51 -0
- data/app/controllers/open_blog/api/v1/report_controller.rb +20 -0
- data/app/controllers/open_blog/api/v1/resources_controller.rb +39 -0
- data/app/controllers/open_blog/api/v1/series_controller.rb +32 -0
- data/app/controllers/open_blog/api/v1/tags_controller.rb +12 -0
- data/app/controllers/open_blog/api/v1/views_controller.rb +19 -0
- data/app/controllers/open_blog/application_controller.rb +5 -0
- data/app/controllers/open_blog/authors_controller.rb +14 -0
- data/app/controllers/open_blog/categories_controller.rb +15 -0
- data/app/controllers/open_blog/errors_controller.rb +7 -0
- data/app/controllers/open_blog/feeds_controller.rb +22 -0
- data/app/controllers/open_blog/mcp_controller.rb +38 -0
- data/app/controllers/open_blog/media_controller.rb +20 -0
- data/app/controllers/open_blog/pages_controller.rb +12 -0
- data/app/controllers/open_blog/posts_controller.rb +57 -0
- data/app/controllers/open_blog/previews_controller.rb +35 -0
- data/app/controllers/open_blog/search_controller.rb +32 -0
- data/app/controllers/open_blog/series_controller.rb +15 -0
- data/app/controllers/open_blog/sitemaps_controller.rb +10 -0
- data/app/controllers/open_blog/tags_controller.rb +15 -0
- data/app/helpers/open_blog/content_helper.rb +79 -0
- data/app/helpers/open_blog/dates_helper.rb +18 -0
- data/app/helpers/open_blog/faq_helper.rb +59 -0
- data/app/helpers/open_blog/head_helper.rb +75 -0
- data/app/helpers/open_blog/icons_helper.rb +10 -0
- data/app/helpers/open_blog/notices_helper.rb +47 -0
- data/app/helpers/open_blog/pagination_helper.rb +18 -0
- data/app/helpers/open_blog/structured_data_helper.rb +63 -0
- data/app/helpers/open_blog/theme_helper.rb +52 -0
- data/app/helpers/open_blog/url_helper.rb +58 -0
- data/app/jobs/open_blog/publish_scheduled_post_job.rb +14 -0
- data/app/models/concerns/open_blog/immutable.rb +13 -0
- data/app/models/concerns/open_blog/immutable_image_attachment.rb +43 -0
- data/app/models/concerns/open_blog/previewable.rb +38 -0
- data/app/models/open_blog/api_token.rb +34 -0
- data/app/models/open_blog/application_record.rb +5 -0
- data/app/models/open_blog/approval.rb +19 -0
- data/app/models/open_blog/author.rb +11 -0
- data/app/models/open_blog/baseline.rb +26 -0
- data/app/models/open_blog/category.rb +10 -0
- data/app/models/open_blog/connection_declaration.rb +16 -0
- data/app/models/open_blog/faq.rb +22 -0
- data/app/models/open_blog/image.rb +45 -0
- data/app/models/open_blog/page.rb +23 -0
- data/app/models/open_blog/page_view.rb +7 -0
- data/app/models/open_blog/post.rb +132 -0
- data/app/models/open_blog/publication.rb +17 -0
- data/app/models/open_blog/redirect.rb +16 -0
- data/app/models/open_blog/revision.rb +12 -0
- data/app/models/open_blog/series.rb +10 -0
- data/app/models/open_blog/tag.rb +11 -0
- data/app/models/open_blog/tagging.rb +7 -0
- data/app/queries/open_blog/api_post_query.rb +54 -0
- data/app/serializers/open_blog/adoption_result_serializer.rb +9 -0
- data/app/serializers/open_blog/author_serializer.rb +10 -0
- data/app/serializers/open_blog/category_serializer.rb +7 -0
- data/app/serializers/open_blog/doctor_serializer.rb +7 -0
- data/app/serializers/open_blog/error_serializer.rb +7 -0
- data/app/serializers/open_blog/extraction_result_serializer.rb +7 -0
- data/app/serializers/open_blog/findings_serializer.rb +13 -0
- data/app/serializers/open_blog/image_serializer.rb +9 -0
- data/app/serializers/open_blog/page_serializer.rb +9 -0
- data/app/serializers/open_blog/post_serializer.rb +59 -0
- data/app/serializers/open_blog/preview_serializer.rb +11 -0
- data/app/serializers/open_blog/records_serializer.rb +28 -0
- data/app/serializers/open_blog/redirect_serializer.rb +7 -0
- data/app/serializers/open_blog/series_serializer.rb +7 -0
- data/app/serializers/open_blog/tag_serializer.rb +7 -0
- data/app/serializers/open_blog/write_result_serializer.rb +9 -0
- data/app/views/open_blog/attachments/_blob.html.erb +1 -0
- data/app/views/open_blog/feeds/atom.xml.builder +27 -0
- data/app/views/open_blog/sitemaps/show.xml.builder +9 -0
- data/config/locales/open_blog.en.yml +95 -0
- data/config/routes.rb +52 -0
- data/db/migrate/20261003012512_create_open_blog_authors.rb +16 -0
- data/db/migrate/20261003012513_create_open_blog_categories.rb +13 -0
- data/db/migrate/20261003012514_create_open_blog_series.rb +11 -0
- data/db/migrate/20261003012515_create_open_blog_images.rb +17 -0
- data/db/migrate/20261003012516_create_open_blog_posts.rb +47 -0
- data/db/migrate/20261003012517_create_open_blog_faqs.rb +13 -0
- data/db/migrate/20261003012518_create_open_blog_tags.rb +17 -0
- data/db/migrate/20261003012519_create_open_blog_redirects.rb +14 -0
- data/db/migrate/20261003012520_create_open_blog_revisions.rb +14 -0
- data/db/migrate/20261003012521_create_open_blog_approvals.rb +21 -0
- data/db/migrate/20261003012522_create_open_blog_publications.rb +18 -0
- data/db/migrate/20261003012523_create_open_blog_baselines.rb +24 -0
- data/db/migrate/20261003012524_create_open_blog_connection_declarations.rb +13 -0
- data/db/migrate/20261003031410_add_body_image_manifest_to_open_blog_posts.rb +7 -0
- data/db/migrate/20261003164418_create_open_blog_api_tokens.rb +15 -0
- data/db/migrate/20261003175454_create_open_blog_pages.rb +16 -0
- data/db/migrate/20261003182919_create_open_blog_page_views.rb +11 -0
- data/lib/generators/open_blog/admin_suite/admin_suite_generator.rb +27 -0
- data/lib/generators/open_blog/admin_suite/templates/author_resource.rb.tt +29 -0
- data/lib/generators/open_blog/admin_suite/templates/blog_portal.rb.tt +16 -0
- data/lib/generators/open_blog/admin_suite/templates/category_resource.rb.tt +28 -0
- data/lib/generators/open_blog/admin_suite/templates/initializer.rb.tt +19 -0
- data/lib/generators/open_blog/admin_suite/templates/page_resource.rb.tt +32 -0
- data/lib/generators/open_blog/admin_suite/templates/post_resource.rb.tt +58 -0
- data/lib/generators/open_blog/generator_support.rb +64 -0
- data/lib/generators/open_blog/install/install_generator.rb +188 -0
- data/lib/generators/open_blog/install/templates/initializer.rb.tt +16 -0
- data/lib/generators/open_blog/install/templates/sample_cover.png +0 -0
- data/lib/generators/open_blog/install/templates/sample_post.md +54 -0
- data/lib/generators/open_blog/install/templates/theme/blog.css +193 -0
- data/lib/generators/open_blog/install/templates/theme/open_blog_theme.css +82 -0
- data/lib/generators/open_blog/install/templates/theme/syntax.css +358 -0
- data/lib/generators/open_blog/install/templates/theme/theme.css +120 -0
- data/lib/generators/open_blog/install/templates/views/layouts/open_blog.html.erb +16 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/authors/show.html.erb +17 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/categories/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/errors/not_found.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/errors/preview_expired.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_check.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_copy.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_link.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_monitor.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_moon.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_share.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_sun.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/pages/show.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_faq.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_highlights.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_preview_bar.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_series_nav.html.erb +12 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_tags.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_toc.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/index.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/show.html.erb +29 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/search/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/series/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_author_box.html.erb +6 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_breadcrumbs.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_call_to_action.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_featured_card.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_footer.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_header.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_pagination.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_post_card.html.erb +9 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_related_posts.html.erb +10 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_search.html.erb +9 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_share.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_sidebar.html.erb +38 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_theme_toggle.html.erb +10 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/tags/show.html.erb +13 -0
- data/lib/generators/open_blog/views/views_generator.rb +21 -0
- data/lib/open_blog/actor.rb +7 -0
- data/lib/open_blog/admin_suite.rb +73 -0
- data/lib/open_blog/adopt/contract.rb +152 -0
- data/lib/open_blog/adopt/snapshot.rb +35 -0
- data/lib/open_blog/adopt.rb +159 -0
- data/lib/open_blog/api_fields.rb +40 -0
- data/lib/open_blog/approve.rb +18 -0
- data/lib/open_blog/authentication.rb +18 -0
- data/lib/open_blog/build_css.rb +53 -0
- data/lib/open_blog/configuration.rb +185 -0
- data/lib/open_blog/configuration_error.rb +4 -0
- data/lib/open_blog/content_guard.rb +77 -0
- data/lib/open_blog/doctor/files.rb +61 -0
- data/lib/open_blog/doctor/javascript.rb +57 -0
- data/lib/open_blog/doctor.rb +177 -0
- data/lib/open_blog/engine.rb +45 -0
- data/lib/open_blog/errors.rb +40 -0
- data/lib/open_blog/faq_extraction.rb +184 -0
- data/lib/open_blog/findings/body_checks.rb +58 -0
- data/lib/open_blog/findings/check.rb +19 -0
- data/lib/open_blog/findings/link_check.rb +78 -0
- data/lib/open_blog/findings/record_checks.rb +94 -0
- data/lib/open_blog/findings/registry.rb +18 -0
- data/lib/open_blog/findings.rb +27 -0
- data/lib/open_blog/image_fetch.rb +115 -0
- data/lib/open_blog/image_import.rb +164 -0
- data/lib/open_blog/image_resolution.rb +171 -0
- data/lib/open_blog/instructions.rb +7 -0
- data/lib/open_blog/json_feed.rb +36 -0
- data/lib/open_blog/label_policy.rb +41 -0
- data/lib/open_blog/markdown_view.rb +57 -0
- data/lib/open_blog/mcp/definition.rb +28 -0
- data/lib/open_blog/mcp/dispatcher.rb +33 -0
- data/lib/open_blog/mcp/image_input.rb +30 -0
- data/lib/open_blog/mcp/registry.rb +139 -0
- data/lib/open_blog/mcp/schemas.rb +72 -0
- data/lib/open_blog/mcp.rb +24 -0
- data/lib/open_blog/not_found.rb +4 -0
- data/lib/open_blog/operation.rb +36 -0
- data/lib/open_blog/page_views.rb +72 -0
- data/lib/open_blog/pagination.rb +18 -0
- data/lib/open_blog/plain_text.rb +36 -0
- data/lib/open_blog/policy_url.rb +9 -0
- data/lib/open_blog/post_attributes.rb +172 -0
- data/lib/open_blog/post_identity.rb +28 -0
- data/lib/open_blog/publish.rb +7 -0
- data/lib/open_blog/reader_dates.rb +21 -0
- data/lib/open_blog/reader_page.rb +21 -0
- data/lib/open_blog/reader_queries.rb +90 -0
- data/lib/open_blog/record_release.rb +66 -0
- data/lib/open_blog/redirect_target.rb +58 -0
- data/lib/open_blog/remove.rb +43 -0
- data/lib/open_blog/renderer/highlighter.rb +44 -0
- data/lib/open_blog/renderer/markdown.rb +14 -0
- data/lib/open_blog/renderer/rich_text.rb +19 -0
- data/lib/open_blog/renderer/sanitizer.rb +72 -0
- data/lib/open_blog/renderer/scrubber.rb +35 -0
- data/lib/open_blog/renderer/transform.rb +115 -0
- data/lib/open_blog/renderer.rb +51 -0
- data/lib/open_blog/result.rb +11 -0
- data/lib/open_blog/revision_payload.rb +67 -0
- data/lib/open_blog/rich_text_guard.rb +26 -0
- data/lib/open_blog/sample.rb +56 -0
- data/lib/open_blog/save_draft.rb +7 -0
- data/lib/open_blog/search.rb +21 -0
- data/lib/open_blog/sitemap_entries.rb +66 -0
- data/lib/open_blog/surface_report/expected_content.rb +27 -0
- data/lib/open_blog/surface_report/http.rb +92 -0
- data/lib/open_blog/surface_report/page_checks.rb +303 -0
- data/lib/open_blog/surface_report/reach_checks.rb +141 -0
- data/lib/open_blog/surface_report/record_checks.rb +248 -0
- data/lib/open_blog/surface_report/robots.rb +49 -0
- data/lib/open_blog/surface_report.rb +223 -0
- data/lib/open_blog/syntax_css.rb +55 -0
- data/lib/open_blog/unpublish.rb +13 -0
- data/lib/open_blog/version.rb +3 -0
- data/lib/open_blog/write_post.rb +205 -0
- data/lib/open_blog.rb +91 -0
- data/lib/tasks/open_blog.rake +71 -0
- data/skills/open-blog-adopt/SKILL.md +12 -0
- data/skills/open-blog-install/SKILL.md +10 -0
- data/skills/open-blog-policy-pages/SKILL.md +10 -0
- data/skills/open-blog-publish/SKILL.md +14 -0
- data/skills/open-blog-report/SKILL.md +10 -0
- data/skills/open-blog-update/SKILL.md +16 -0
- metadata +515 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= render "open_blog/shared/breadcrumbs" %>
|
|
2
|
+
<header class="ob-list-heading">
|
|
3
|
+
<h1><%= open_blog_translate("search.heading") %></h1>
|
|
4
|
+
<p class="ob-list-description"><%= open_blog_translate("search.description") %></p>
|
|
5
|
+
</header>
|
|
6
|
+
<div class="ob-list-layout">
|
|
7
|
+
<%= render "open_blog/shared/sidebar" %>
|
|
8
|
+
<div class="ob-list-main">
|
|
9
|
+
<% if @posts.empty? %><p class="ob-empty"><%= open_blog_translate("search.empty") %></p><% end %>
|
|
10
|
+
<div class="ob-post-grid"><%= render partial: "open_blog/shared/post_card", collection: @posts, as: :post %></div>
|
|
11
|
+
<%= render "open_blog/shared/pagination" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= render "open_blog/shared/breadcrumbs" %>
|
|
2
|
+
<header class="ob-list-heading">
|
|
3
|
+
<h1><%= @open_blog_page.record.name %></h1>
|
|
4
|
+
<p class="ob-list-description"><%= open_blog_page_description(@open_blog_page) %></p>
|
|
5
|
+
</header>
|
|
6
|
+
<div class="ob-list-layout">
|
|
7
|
+
<%= render "open_blog/shared/sidebar" %>
|
|
8
|
+
<div class="ob-list-main">
|
|
9
|
+
<% if @posts.empty? %><p class="ob-empty"><%= open_blog_translate("lists.empty") %></p><% end %>
|
|
10
|
+
<div class="ob-post-grid"><%= render partial: "open_blog/shared/post_card", collection: @posts, as: :post %></div>
|
|
11
|
+
<%= render "open_blog/shared/pagination" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<section class="ob-author-box" aria-labelledby="ob-author-heading">
|
|
2
|
+
<h2 id="ob-author-heading"><%= open_blog_translate("headings.about_author") %></h2>
|
|
3
|
+
<%= open_blog_avatar(post.author, name: post.author_name) %>
|
|
4
|
+
<%= link_to post.author_name, open_blog_list_path(:author, post.author) %>
|
|
5
|
+
<% if post.author.bio.present? %><p class="ob-author-bio"><%= post.author.bio %></p><% end %>
|
|
6
|
+
</section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= open_blog_breadcrumbs(@open_blog_page) %>
|
data/lib/generators/open_blog/install/templates/views/open_blog/shared/_call_to_action.html.erb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<% if (cta = OpenBlog.config.call_to_action).present? && OpenBlog::Renderer::Sanitizer.safe_url?(cta[:url]) %>
|
|
2
|
+
<section class="ob-call-to-action" aria-labelledby="ob-call-to-action-heading">
|
|
3
|
+
<h2 id="ob-call-to-action-heading"><%= cta[:title] %></h2>
|
|
4
|
+
<p><%= cta[:text] %></p>
|
|
5
|
+
<%= link_to cta[:label], cta[:url] %>
|
|
6
|
+
</section>
|
|
7
|
+
<% end %>
|
data/lib/generators/open_blog/install/templates/views/open_blog/shared/_featured_card.html.erb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<article class="ob-featured-card">
|
|
2
|
+
<%= link_to post.path, aria: { label: post.title }, class: "ob-card-media" do %><%= open_blog_card_image(post) %><% end %>
|
|
3
|
+
<div class="ob-featured-details">
|
|
4
|
+
<p class="ob-featured-label"><%= open_blog_translate("cards.featured") %></p>
|
|
5
|
+
<h2><%= link_to post.title, post.path %></h2>
|
|
6
|
+
<p class="ob-card-description"><%= post.description %></p>
|
|
7
|
+
<div class="ob-card-author"><%= open_blog_avatar(post.author, name: post.author_name) %> <%= link_to post.author_name, open_blog_list_path(:author, post.author) %></div>
|
|
8
|
+
<%= open_blog_dates(post) %>
|
|
9
|
+
<%= open_blog_reading_time(post) %>
|
|
10
|
+
</div>
|
|
11
|
+
</article>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<footer class="ob-footer">
|
|
2
|
+
<nav aria-label="<%= open_blog_translate("navigation.footer") %>">
|
|
3
|
+
<% OpenBlog::Page::KINDS.each do |kind| %>
|
|
4
|
+
<% url = OpenBlog.policy_url(kind) %>
|
|
5
|
+
<% if url.present? && OpenBlog::Renderer::Sanitizer.safe_url?(url) %>
|
|
6
|
+
<%= link_to open_blog_translate("policies.#{kind}"), url %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<%= link_to open_blog_translate("navigation.feed"), "#{open_blog_index_path.chomp('/')}/feed.xml" %>
|
|
10
|
+
</nav>
|
|
11
|
+
</footer>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<header class="ob-header">
|
|
2
|
+
<%= link_to OpenBlog.config.site_name, open_blog_index_path, class: "ob-site-name" %>
|
|
3
|
+
<nav aria-label="<%= open_blog_translate("navigation.blog") %>">
|
|
4
|
+
<%= link_to OpenBlog.config.blog_title, open_blog_index_path %>
|
|
5
|
+
</nav>
|
|
6
|
+
<%= render "open_blog/shared/theme_toggle" %>
|
|
7
|
+
</header>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= open_blog_paginate(@open_blog_page.pagination) %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<article class="ob-post-card">
|
|
2
|
+
<%= link_to post.path, aria: { label: post.title }, class: "ob-card-media" do %><%= open_blog_card_image(post) %><% end %>
|
|
3
|
+
<% if post.category %><%= link_to post.category.name, open_blog_list_path(:category, post.category), class: "ob-card-category" %><% end %>
|
|
4
|
+
<%= content_tag(local_assigns.fetch(:heading, :h2), class: "ob-card-title") do %><%= link_to post.title, post.path %><% end %>
|
|
5
|
+
<p class="ob-card-description"><%= post.description %></p>
|
|
6
|
+
<div class="ob-card-author"><%= open_blog_avatar(post.author, name: post.author_name) %> <%= link_to post.author_name, open_blog_list_path(:author, post.author) %></div>
|
|
7
|
+
<%= open_blog_dates(post) %>
|
|
8
|
+
<%= open_blog_reading_time(post) %>
|
|
9
|
+
</article>
|
data/lib/generators/open_blog/install/templates/views/open_blog/shared/_related_posts.html.erb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% cache_unless @open_blog_page.kind == :preview, @related_cache_key do %>
|
|
2
|
+
<% if @related_posts.any? %>
|
|
3
|
+
<section class="ob-related-posts" aria-labelledby="ob-related-heading">
|
|
4
|
+
<h2 id="ob-related-heading"><%= open_blog_translate("headings.related") %></h2>
|
|
5
|
+
<div class="ob-post-grid">
|
|
6
|
+
<%= render partial: "open_blog/shared/post_card", collection: @related_posts, as: :post, locals: { heading: :h3 } %>
|
|
7
|
+
</div>
|
|
8
|
+
</section>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% search_path = "#{OpenBlog.mount_path.chomp('/')}/search" %>
|
|
2
|
+
<form action="<%= search_path %>" method="get" role="search" class="ob-search" data-controller="open-blog--search" data-open-blog--search-url-value="<%= search_path %>.json">
|
|
3
|
+
<label for="open-blog-search"><%= open_blog_translate("search.label") %></label>
|
|
4
|
+
<div class="ob-search-controls">
|
|
5
|
+
<input id="open-blog-search" type="search" name="q" maxlength="100" value="<%= @query %>" data-open-blog--search-target="input" data-action="input->open-blog--search#search" autocomplete="off">
|
|
6
|
+
<button type="submit"><%= open_blog_translate("search.submit") %></button>
|
|
7
|
+
</div>
|
|
8
|
+
<ul class="ob-search-results" aria-label="<%= open_blog_translate("search.suggestions") %>" aria-live="polite" data-open-blog--search-target="results" hidden></ul>
|
|
9
|
+
</form>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<nav class="ob-share" aria-label="<%= open_blog_translate("share.heading") %>"
|
|
2
|
+
data-controller="open-blog--share"
|
|
3
|
+
data-open-blog--share-url-value="<%= open_blog_absolute_url(post.path) %>"
|
|
4
|
+
data-open-blog--share-title-value="<%= post.title %>"
|
|
5
|
+
data-open-blog--share-copy-label-value="<%= open_blog_translate("share.copy") %>"
|
|
6
|
+
data-open-blog--share-copied-label-value="<%= open_blog_translate("share.copied") %>"
|
|
7
|
+
data-open-blog--share-error-label-value="<%= open_blog_translate("share.failed") %>">
|
|
8
|
+
<button type="button" class="ob-share-button" hidden data-open-blog--share-target="device" data-action="open-blog--share#share"><%= open_blog_icon("share") %> <%= open_blog_translate("share.device") %></button>
|
|
9
|
+
<%= link_to open_blog_translate("share.x"), "https://twitter.com/intent/tweet?url=#{ERB::Util.url_encode(open_blog_absolute_url(post.path))}&text=#{ERB::Util.url_encode(post.title)}", rel: "noopener" %>
|
|
10
|
+
<%= link_to open_blog_translate("share.linkedin"), "https://www.linkedin.com/sharing/share-offsite/?url=#{ERB::Util.url_encode(open_blog_absolute_url(post.path))}", rel: "noopener" %>
|
|
11
|
+
<button type="button" class="ob-share-button" hidden data-open-blog--share-target="copy" data-action="open-blog--share#copy"><%= open_blog_icon("copy") %> <%= open_blog_translate("share.copy") %></button>
|
|
12
|
+
<span class="ob-share-status" data-open-blog--share-target="status" aria-live="polite"></span>
|
|
13
|
+
</nav>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<aside class="ob-sidebar">
|
|
2
|
+
<%= render "open_blog/shared/search" %>
|
|
3
|
+
<% cache @sidebar_cache_key do %>
|
|
4
|
+
<nav class="ob-sidebar-categories" aria-label="<%= open_blog_translate("navigation.categories") %>">
|
|
5
|
+
<h2><%= open_blog_translate("navigation.categories") %></h2>
|
|
6
|
+
<ul>
|
|
7
|
+
<li><%= link_to open_blog_translate("navigation.all_posts"), open_blog_index_path, aria: { current: (@open_blog_page.kind == :index ? "page" : nil) } %></li>
|
|
8
|
+
<% @sidebar_categories.each do |entry| %>
|
|
9
|
+
<li>
|
|
10
|
+
<%= link_to open_blog_list_path(:category, entry[:category]), aria: { current: (@open_blog_page.kind == :category && @open_blog_page.record == entry[:category] ? "page" : nil) } do %>
|
|
11
|
+
<span><%= entry[:category].name %></span> <span class="ob-count"><%= entry[:count] %></span>
|
|
12
|
+
<% end %>
|
|
13
|
+
</li>
|
|
14
|
+
<% end %>
|
|
15
|
+
</ul>
|
|
16
|
+
</nav>
|
|
17
|
+
<% if @sidebar_tags.any? %>
|
|
18
|
+
<nav class="ob-sidebar-tags" aria-label="<%= open_blog_translate("navigation.tags") %>">
|
|
19
|
+
<h2><%= open_blog_translate("navigation.tags") %></h2>
|
|
20
|
+
<ul>
|
|
21
|
+
<% @sidebar_tags.each do |entry| %>
|
|
22
|
+
<li><%= link_to entry[:tag].name, open_blog_list_path(:tag, entry[:tag]), aria: { current: (@open_blog_page.kind == :tag && @open_blog_page.record == entry[:tag] ? "page" : nil) } %></li>
|
|
23
|
+
<% end %>
|
|
24
|
+
</ul>
|
|
25
|
+
</nav>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% if @popular_posts.any? %>
|
|
29
|
+
<nav class="ob-sidebar-popular" aria-label="<%= open_blog_translate("navigation.popular") %>">
|
|
30
|
+
<h2><%= open_blog_translate("navigation.popular") %></h2>
|
|
31
|
+
<ol>
|
|
32
|
+
<% @popular_posts.each do |post| %>
|
|
33
|
+
<li><%= link_to post.title, post.path %></li>
|
|
34
|
+
<% end %>
|
|
35
|
+
</ol>
|
|
36
|
+
</nav>
|
|
37
|
+
<% end %>
|
|
38
|
+
</aside>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% unless open_blog_fixed_theme %>
|
|
2
|
+
<button type="button" class="ob-theme-toggle" hidden
|
|
3
|
+
data-controller="open-blog--theme" data-action="open-blog--theme#cycle"
|
|
4
|
+
data-open-blog--theme-system-label-value="<%= open_blog_translate("theme.system") %>"
|
|
5
|
+
data-open-blog--theme-light-label-value="<%= open_blog_translate("theme.light") %>"
|
|
6
|
+
data-open-blog--theme-dark-label-value="<%= open_blog_translate("theme.dark") %>">
|
|
7
|
+
<%= open_blog_icon("monitor") %>
|
|
8
|
+
<span data-open-blog--theme-target="label"><%= open_blog_translate("theme.system") %></span>
|
|
9
|
+
</button>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%= render "open_blog/shared/breadcrumbs" %>
|
|
2
|
+
<header class="ob-list-heading">
|
|
3
|
+
<h1><%= open_blog_translate("titles.tag", name: @open_blog_page.record.name) %></h1>
|
|
4
|
+
<p class="ob-list-description"><%= open_blog_page_description(@open_blog_page) %></p>
|
|
5
|
+
</header>
|
|
6
|
+
<div class="ob-list-layout">
|
|
7
|
+
<%= render "open_blog/shared/sidebar" %>
|
|
8
|
+
<div class="ob-list-main">
|
|
9
|
+
<% if @posts.empty? %><p class="ob-empty"><%= open_blog_translate("lists.empty") %></p><% end %>
|
|
10
|
+
<div class="ob-post-grid"><%= render partial: "open_blog/shared/post_card", collection: @posts, as: :post %></div>
|
|
11
|
+
<%= render "open_blog/shared/pagination" %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require_relative "../generator_support"
|
|
3
|
+
|
|
4
|
+
module OpenBlog
|
|
5
|
+
module Generators
|
|
6
|
+
class ViewsGenerator < Rails::Generators::Base
|
|
7
|
+
include GeneratorSupport
|
|
8
|
+
source_root File.expand_path("../install/templates", __dir__)
|
|
9
|
+
class_option :skip_tailwind, type: :boolean, default: nil
|
|
10
|
+
|
|
11
|
+
def copy_views
|
|
12
|
+
css = if options[:skip_tailwind].nil? && host_read("app/views/layouts/open_blog.html.erb").include?("open_blog_stylesheets")
|
|
13
|
+
:fallback
|
|
14
|
+
else
|
|
15
|
+
css_setup
|
|
16
|
+
end
|
|
17
|
+
copy_reader_views(css: css)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require "admin_suite"
|
|
2
|
+
require "admin_suite/ui/field_renderer_registry"
|
|
3
|
+
|
|
4
|
+
module OpenBlog
|
|
5
|
+
module AdminSuite
|
|
6
|
+
RESOURCES = { "open_blog_posts" => "Post", "open_blog_categories" => "Category",
|
|
7
|
+
"open_blog_authors" => "Author", "open_blog_pages" => "Page" }.freeze
|
|
8
|
+
FAQ_FIELDS = %i[id question answer position _destroy].freeze
|
|
9
|
+
|
|
10
|
+
module ResourceParameters
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def resource_config
|
|
14
|
+
name = RESOURCES[resource_name]
|
|
15
|
+
return super unless current_portal.to_s == "open_blog" && name
|
|
16
|
+
ensure_resources_loaded!
|
|
17
|
+
definition = "::Admin::Resources::OpenBlog::#{name}Resource".safe_constantize
|
|
18
|
+
return unless definition.is_a?(Class) && definition < ::Admin::Base::Resource
|
|
19
|
+
definition if definition.model_class == "::OpenBlog::#{name}".constantize
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resource_params
|
|
23
|
+
fields = resource_config&.form_config&.fields_list || []
|
|
24
|
+
nested = resource_class == ::OpenBlog::Post && fields.any? do |field|
|
|
25
|
+
field.respond_to?(:name) && field.name.to_sym == :faqs_attributes && field.type == :open_blog_faq
|
|
26
|
+
end
|
|
27
|
+
return super unless nested
|
|
28
|
+
|
|
29
|
+
original = params
|
|
30
|
+
key = resource_class.model_name.param_key
|
|
31
|
+
faq = original.require(key).slice(:faqs_attributes).permit(faqs_attributes: FAQ_FIELDS)
|
|
32
|
+
filtered = original.deep_dup
|
|
33
|
+
filtered[key].delete(:faqs_attributes)
|
|
34
|
+
self.params = filtered
|
|
35
|
+
super.merge(faq)
|
|
36
|
+
ensure
|
|
37
|
+
self.params = original if original
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.install!
|
|
42
|
+
return if @installed
|
|
43
|
+
::AdminSuite::UI::FieldRendererRegistry.register(:open_blog_faq) do |view, form, field, resource, field_class|
|
|
44
|
+
render_faqs(view, form, field, resource, field_class)
|
|
45
|
+
end
|
|
46
|
+
Rails.application.reloader.to_prepare { install_controller! }
|
|
47
|
+
install_controller!
|
|
48
|
+
@installed = true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.install_controller!
|
|
52
|
+
controller = ::AdminSuite::ResourcesController
|
|
53
|
+
controller.prepend(ResourceParameters) unless controller.ancestors.include?(ResourceParameters)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.render_faqs(view, form, field, resource, field_class)
|
|
57
|
+
entries = resource.faqs.to_a
|
|
58
|
+
next_position = entries.filter_map(&:position).max.to_i + 1
|
|
59
|
+
entries += [ ::OpenBlog::Faq.new(position: next_position) ] unless field.readonly
|
|
60
|
+
form.fields_for(:faqs, entries, include_id: false) do |nested|
|
|
61
|
+
view.tag.fieldset(class: "open-blog-faq-fields") do
|
|
62
|
+
view.safe_join([
|
|
63
|
+
view.tag.legend(nested.object.persisted? ? "FAQ #{nested.object.position}" : "Add a FAQ"),
|
|
64
|
+
nested.hidden_field(:id), nested.label(:position), nested.number_field(:position, min: 1, class: field_class, readonly: field.readonly),
|
|
65
|
+
nested.label(:question), nested.text_field(:question, class: field_class, readonly: field.readonly),
|
|
66
|
+
nested.label(:answer), nested.text_area(:answer, class: field_class, rows: 4, readonly: field.readonly),
|
|
67
|
+
nested.check_box(:_destroy, disabled: field.readonly), nested.label(:_destroy, "Remove this FAQ")
|
|
68
|
+
])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class Adopt
|
|
3
|
+
class Contract
|
|
4
|
+
CONTENT_FIELDS = (PostAttributes::CONTENT_FIELDS + %i[provenance provenance_evidence]).freeze
|
|
5
|
+
EXTRA_FIELDS = %i[source_system source_id source_body_sha256 old_slugs category_description first_published_at first_published_evidence last_modified_at last_modified_evidence imported_approval declaration connections dry_run].freeze
|
|
6
|
+
attr_reader :attributes, :baseline, :approval, :connections
|
|
7
|
+
|
|
8
|
+
def initialize(input, now: Time.current)
|
|
9
|
+
@now = now
|
|
10
|
+
invalid("attributes") unless input.is_a?(Hash) && input.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
11
|
+
@attributes = input.symbolize_keys
|
|
12
|
+
unknown = @attributes.keys - CONTENT_FIELDS - EXTRA_FIELDS
|
|
13
|
+
raise Error::UnknownField.new(details: unknown.map(&:to_s)) if unknown.any?
|
|
14
|
+
%i[source_system source_id slug title body_format].each { |field| required_text(@attributes, field, field.to_s) }
|
|
15
|
+
invalid("body") unless @attributes[:body].is_a?(String)
|
|
16
|
+
raise Error::SlugNotSupported.new(details: [ "slug" ]) unless Post::SLUG_PATTERN.match?(@attributes[:slug])
|
|
17
|
+
invalid("dry_run") if @attributes.key?(:dry_run) && ![ true, false ].include?(@attributes[:dry_run])
|
|
18
|
+
%i[category_description provenance_evidence first_published_evidence last_modified_evidence].each do |field|
|
|
19
|
+
invalid(field.to_s) if @attributes.key?(field) && !@attributes[field].nil? && !@attributes[field].is_a?(String)
|
|
20
|
+
end
|
|
21
|
+
if !@attributes[:source_body_sha256].nil? && !(@attributes[:source_body_sha256].is_a?(String) && @attributes[:source_body_sha256].match?(/\A[0-9a-f]{64}\z/))
|
|
22
|
+
invalid("source_body_sha256")
|
|
23
|
+
end
|
|
24
|
+
normalize_approval
|
|
25
|
+
normalize_baseline
|
|
26
|
+
normalize_connections
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dry_run?
|
|
30
|
+
@attributes[:dry_run] == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def content
|
|
34
|
+
defaults = { description: "", search_title: "", search_description: "", cover_alt: "", faq: [], tags: [],
|
|
35
|
+
category: nil, series: nil, series_position: nil, featured: false, canonical_url: nil, external_id: nil,
|
|
36
|
+
cover_image: nil, social_image: nil, author: OpenBlog.config.default_author, provenance: "unknown", provenance_evidence: nil }
|
|
37
|
+
defaults.merge(@attributes.slice(*CONTENT_FIELDS)).merge(provenance: @baseline[:provenance], provenance_evidence: @baseline[:provenance_evidence])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def redirects(default_date:, current_slug:)
|
|
41
|
+
value = @attributes.fetch(:old_slugs, [])
|
|
42
|
+
invalid("old_slugs") unless value.is_a?(Array)
|
|
43
|
+
value.map do |item|
|
|
44
|
+
entry = item.is_a?(String) ? { slug: item } : nested(item, %i[slug moved_on], "old_slugs")
|
|
45
|
+
slug = required_text(entry, :slug, "old_slugs.slug")
|
|
46
|
+
unless Post::SLUG_PATTERN.match?(slug)
|
|
47
|
+
raise Error::SlugNotSupported.new(details: [ "old_slugs.slug" ])
|
|
48
|
+
end
|
|
49
|
+
invalid("old_slugs.slug") if slug == current_slug
|
|
50
|
+
{ old_path: "#{OpenBlog.mount_path.chomp('/')}/#{slug}", new_path: "#{OpenBlog.mount_path.chomp('/')}/#{current_slug}",
|
|
51
|
+
occurred_on: entry[:moved_on].nil? ? default_date : date(entry[:moved_on], "old_slugs.moved_on") }
|
|
52
|
+
end.uniq.sort_by { |entry| entry[:old_path] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def invalid(field)
|
|
58
|
+
raise Error::ValidationFailed.new(details: Array(field))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def required_text(input, field, path)
|
|
62
|
+
value = input[field]
|
|
63
|
+
invalid(path) unless value.is_a?(String) && value.present?
|
|
64
|
+
value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def nested(value, allowed, path)
|
|
68
|
+
invalid(path) unless value.is_a?(Hash) && value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
69
|
+
result = value.symbolize_keys
|
|
70
|
+
unknown = result.keys - allowed
|
|
71
|
+
raise Error::UnknownField.new(details: unknown.map { |field| "#{path}.#{field}" }) if unknown.any?
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def timestamp(value, path)
|
|
76
|
+
return if value.nil?
|
|
77
|
+
invalid(path) unless value.is_a?(String) || value.is_a?(Time) || value.is_a?(ActiveSupport::TimeWithZone) || value.is_a?(DateTime)
|
|
78
|
+
result = value.is_a?(String) ? Time.iso8601(value) : value.to_time
|
|
79
|
+
result.utc.floor(6)
|
|
80
|
+
rescue ArgumentError, TypeError
|
|
81
|
+
invalid(path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def date(value, path)
|
|
85
|
+
invalid(path) unless value.is_a?(String) || value.is_a?(Date)
|
|
86
|
+
invalid(path) if value.is_a?(String) && !value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
87
|
+
value.is_a?(String) ? Date.iso8601(value) : value.to_date
|
|
88
|
+
rescue ArgumentError, TypeError
|
|
89
|
+
invalid(path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def normalize_approval
|
|
93
|
+
if @attributes.key?(:declaration) && @attributes.key?(:imported_approval)
|
|
94
|
+
invalid(%w[declaration imported_approval])
|
|
95
|
+
end
|
|
96
|
+
key = @attributes.key?(:declaration) ? :declaration : (@attributes.key?(:imported_approval) ? :imported_approval : nil)
|
|
97
|
+
return unless key
|
|
98
|
+
allowed = %i[reviewer_name approved_at facts_checked] + (key == :declaration ? %i[declared_on declared_by declared_first_published_at] : %i[evidence confirmed_by])
|
|
99
|
+
value = nested(@attributes[key], allowed, key.to_s)
|
|
100
|
+
unless value[:reviewer_name].is_a?(String) && value[:reviewer_name].present? && [ true, false ].include?(value[:facts_checked])
|
|
101
|
+
raise Error::ApprovalIncomplete
|
|
102
|
+
end
|
|
103
|
+
invalid("#{key}.approved_at") unless value[:approved_at]
|
|
104
|
+
value[:approved_at] = timestamp(value[:approved_at], "#{key}.approved_at")
|
|
105
|
+
if key == :declaration
|
|
106
|
+
required_text(value, :declared_by, "declaration.declared_by")
|
|
107
|
+
value[:declared_on] = date(value[:declared_on], "declaration.declared_on")
|
|
108
|
+
@declared_first = timestamp(value.delete(:declared_first_published_at), "declaration.declared_first_published_at")
|
|
109
|
+
invalid("declaration.declared_first_published_at") if @attributes[:first_published_at] && @declared_first
|
|
110
|
+
else
|
|
111
|
+
required_text(value, :evidence, "imported_approval.evidence")
|
|
112
|
+
required_text(value, :confirmed_by, "imported_approval.confirmed_by")
|
|
113
|
+
end
|
|
114
|
+
@approval = value.merge(kind: key == :declaration ? "declared" : "imported")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def normalize_baseline
|
|
118
|
+
provenance = @attributes.fetch(:provenance, "unknown").to_s
|
|
119
|
+
invalid("provenance") unless %w[ai_assisted human_written unknown].include?(provenance)
|
|
120
|
+
evidence = @attributes[:provenance_evidence]
|
|
121
|
+
if evidence.blank? && @approval && @approval[:kind] == "declared"
|
|
122
|
+
evidence = "Declared by #{@approval[:declared_by]} on #{@approval[:declared_on].iso8601}"
|
|
123
|
+
end
|
|
124
|
+
invalid("provenance_evidence") if provenance != "unknown" && evidence.blank?
|
|
125
|
+
@baseline = @attributes.slice(:source_system, :source_id, :source_body_sha256).merge(
|
|
126
|
+
source_body_sha256: @attributes[:source_body_sha256], provenance: provenance,
|
|
127
|
+
provenance_evidence: evidence, declared_first_published_at: @declared_first)
|
|
128
|
+
%i[first_published last_modified].each do |name|
|
|
129
|
+
time = timestamp(@attributes[:"#{name}_at"], "#{name}_at")
|
|
130
|
+
evidence = @attributes[:"#{name}_evidence"].presence
|
|
131
|
+
@baseline[:"#{name}_at"] = evidence ? time : nil
|
|
132
|
+
@baseline[:"#{name}_evidence"] = evidence
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def normalize_connections
|
|
137
|
+
return unless @attributes.key?(:connections)
|
|
138
|
+
value = nested(@attributes[:connections], %i[connections third_party_paid declared_by declared_on], "connections")
|
|
139
|
+
required_text(value, :declared_by, "connections.declared_by")
|
|
140
|
+
invalid("connections.third_party_paid") unless [ true, false ].include?(value[:third_party_paid])
|
|
141
|
+
value[:declared_on] = value.key?(:declared_on) ? date(value[:declared_on], "connections.declared_on") : @now.to_date
|
|
142
|
+
invalid("connections.connections") unless value[:connections].is_a?(Array)
|
|
143
|
+
value[:connections] = value[:connections].map do |entry|
|
|
144
|
+
normalized = nested(entry, %i[party relation], "connections.connections")
|
|
145
|
+
%i[party relation].each { |field| required_text(normalized, field, "connections.connections.#{field}") }
|
|
146
|
+
normalized.stringify_keys
|
|
147
|
+
end
|
|
148
|
+
@connections = value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class Adopt
|
|
3
|
+
class Snapshot
|
|
4
|
+
BASELINE_FIELDS = %i[source_system source_id source_body_sha256 provenance provenance_evidence first_published_at first_published_evidence last_modified_at last_modified_evidence declared_first_published_at].freeze
|
|
5
|
+
APPROVAL_FIELDS = %i[kind reviewer_name facts_checked approved_at declared_on declared_by confirmed_by evidence].freeze
|
|
6
|
+
CONNECTION_FIELDS = %i[connections third_party_paid declared_by declared_on].freeze
|
|
7
|
+
POST_FIELDS = %w[slug body_format author_id category_id series_id series_position featured canonical_url external_id provenance provenance_evidence status].freeze
|
|
8
|
+
|
|
9
|
+
def self.stored(post, baseline, include_connections:)
|
|
10
|
+
return unless baseline
|
|
11
|
+
capture(post,
|
|
12
|
+
baseline: baseline.attributes.symbolize_keys.slice(*BASELINE_FIELDS),
|
|
13
|
+
approvals: post.approvals.where(revision_id: baseline.adopted_revision_id).order(:id).map { |record| record.attributes.symbolize_keys.slice(*APPROVAL_FIELDS) },
|
|
14
|
+
redirects: Redirect.where(post: post, source: "adoption").order(:old_path).map { |record| record.attributes.symbolize_keys.slice(:old_path, :new_path, :occurred_on) },
|
|
15
|
+
connections: include_connections ? latest_connections(post) : nil)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.candidate(post, contract, redirects)
|
|
19
|
+
approval = contract.approval && APPROVAL_FIELDS.index_with { |field| contract.approval[field] }
|
|
20
|
+
capture(post, baseline: contract.baseline, approvals: [ approval ].compact,
|
|
21
|
+
redirects: redirects, connections: contract.connections)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.latest_connections(post)
|
|
25
|
+
post.connection_declarations.order(:id).last&.attributes&.symbolize_keys&.slice(*CONNECTION_FIELDS)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.capture(post, baseline:, approvals:, redirects:, connections:)
|
|
29
|
+
{ identifier: RevisionPayload.new(post).identifier, post: post.attributes.slice(*POST_FIELDS),
|
|
30
|
+
category_description: post.category&.description, tags: post.taggings.reject(&:marked_for_destruction?).map(&:tag_id).sort,
|
|
31
|
+
baseline: baseline, approvals: approvals, redirects: redirects, connections: connections }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|