diff --git a/app/commands/auto_tagging/tag.rb b/app/commands/auto_tagging/tag.rb new file mode 100644 index 0000000..23fca6f --- /dev/null +++ b/app/commands/auto_tagging/tag.rb @@ -0,0 +1,31 @@ +module Adamantium + module Commands + module AutoTagging + class Tag + include Dry::Monads[:result] + include Deps["repos.post_repo", "repos.auto_tagging_repo"] + + def call(auto_tag_id: nil) + auto_taggings = if auto_tag_id + auto_tagging_repo.find(auto_tag_id) + else + auto_tagging_repo.all + end + + auto_taggings.each do |auto_tagging| + posts = auto_tagging.title_only? ? + post_repo.by_title(title_contains: auto_tagging.title_contains) : + post_repo.by_content(body_contains: auto_tagging.body_contains) + + posts.each do |post| + post_repo.auto_tag_post(post_id: post.id, + tag_id: auto_tagging.tag_id) + end + end + + Success() + end + end + end + end +end diff --git a/app/commands/posts/create_entry.rb b/app/commands/posts/create_entry.rb index 6585992..b0cdb83 100644 --- a/app/commands/posts/create_entry.rb +++ b/app/commands/posts/create_entry.rb @@ -10,6 +10,7 @@ module Adamantium syndicate: "commands.posts.syndicate", send_to_dayone: "syndication.dayone", send_webmentions: "commands.posts.send_webmentions", + auto_tag: "commands.auto_tagging.tag", ] include Dry::Monads[:result] @@ -20,6 +21,8 @@ module Adamantium syndicate.call(created_post.id, post) + auto_tag.call() + # decorated_post = Decorators::Posts::Decorator.new(created_post) # send_webmentions.call(post_content: attrs[:content], post_url: decorated_post.permalink) diff --git a/app/entities/auto_tagging.rb b/app/entities/auto_tagging.rb new file mode 100644 index 0000000..566feb9 --- /dev/null +++ b/app/entities/auto_tagging.rb @@ -0,0 +1,13 @@ +module Adamantium + module Entities + class AutoTagging < Dry::Struct + attribute? :title_contains, Types::Optional::String + attribute? :body_contains, Types::Optional::String + attribute :tag_id, Types::Coercible::Integer + + def title_only? + !title_contains.empty? + end + end + end +end diff --git a/app/repos/auto_tagging_repo.rb b/app/repos/auto_tagging_repo.rb new file mode 100644 index 0000000..8d776df --- /dev/null +++ b/app/repos/auto_tagging_repo.rb @@ -0,0 +1,18 @@ +module Adamantium + module Repos + class AutoTaggingRepo < Adamantium::Repo[:auto_taggings] + def find(id) + auto_taggings + .where(id: id) + .map_to(Adamantium::Entities::AutoTagging) + .to_a + end + + def all + auto_taggings + .map_to(Adamantium::Entities::AutoTagging) + .to_a + end + end + end +end diff --git a/app/repos/post_repo.rb b/app/repos/post_repo.rb index 9c8d35a..2534caa 100644 --- a/app/repos/post_repo.rb +++ b/app/repos/post_repo.rb @@ -47,6 +47,38 @@ module Adamantium end end + def auto_tag_post(post_id:, tag_id:) + + return if posts + .post_tags + .where( + post_id: post_id, + tag_id: tag_id + ).count > 0 + + posts + .post_tags + .changeset(:create, { + post_id: post_id, + tag_id: tag_id + }) + .commit + end + + def by_title(title_contains:) + posts + .where(post_type: "post") + .published + .where(Sequel.ilike(:name, "%#{title_contains}%")).to_a + end + + def by_content(body_contains:) + posts + .where(post_type: "post") + .published + .where(Sequel.ilike(:content, "%#{body_contains}%")).to_a + end + def remove_tag(post_id:, tag:) tag = posts.tags.where(label: tag).one diff --git a/config/routes.rb b/config/routes.rb index cb6eec7..672e7b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,6 +60,11 @@ module Adamantium get "/tags", to: "tags.index" delete "/tags/:id", to: "tags.delete" + + get "/tags/auto_tagging", to: "auto_tagging.index" + get "/tags/auto_tagging/new", to: "auto_tagging.new" + post "/tags/auto_tagging", to: "auto_tagging.create" + delete "/tags/auto_taggings/:id", to: "auto_tagging.delete" end end end diff --git a/db/migrate/20230506081449_create_auto_taggings.rb b/db/migrate/20230506081449_create_auto_taggings.rb new file mode 100644 index 0000000..e4af27d --- /dev/null +++ b/db/migrate/20230506081449_create_auto_taggings.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table(:auto_taggings) do + primary_key :id + column :title_contains, :text + column :body_contains, :text + foreign_key :tag_id, :tags, null: false + end + end +end diff --git a/lib/adamantium/persistence/relations/auto_taggings.rb b/lib/adamantium/persistence/relations/auto_taggings.rb new file mode 100644 index 0000000..bb1e8a2 --- /dev/null +++ b/lib/adamantium/persistence/relations/auto_taggings.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Adamantium + module Persistence + module Relations + class AutoTaggings < ROM::Relation[:sql] + schema :auto_taggings, infer: true do + associations do + belongs_to :tag + end + end + + auto_struct(true) + end + end + end +end diff --git a/slices/admin/actions/auto_tagging/create.rb b/slices/admin/actions/auto_tagging/create.rb new file mode 100644 index 0000000..dc1f732 --- /dev/null +++ b/slices/admin/actions/auto_tagging/create.rb @@ -0,0 +1,28 @@ +module Admin + module Actions + module AutoTagging + class Create < Action + + include Deps["commands.auto_tagging.create", + "views.auto_tagging.index", + auto_tag: "commands.auto_tagging.tag"] + + def handle(req, res) + title_contains = req.params[:title_contains] + body_contains = req.params[:body_contains] + tag_id = req.params[:tag_id] + + result = create.(title_contains: title_contains, + body_contains: body_contains, + tag_id: tag_id) + + if result.success? + auto_tag.(auto_tag_id: result.value!) + end + + res.render index + end + end + end + end +end \ No newline at end of file diff --git a/slices/admin/actions/auto_tagging/delete.rb b/slices/admin/actions/auto_tagging/delete.rb new file mode 100644 index 0000000..ca3240a --- /dev/null +++ b/slices/admin/actions/auto_tagging/delete.rb @@ -0,0 +1,14 @@ +module Admin + module Actions + module AutoTagging + class Delete < Action + + include Deps["repos.auto_tagging_repo"] + + def handle(req, res) + auto_tagging_repo.delete(id: req.params[:id]) + end + end + end + end +end \ No newline at end of file diff --git a/slices/admin/actions/auto_tagging/index.rb b/slices/admin/actions/auto_tagging/index.rb new file mode 100644 index 0000000..d9efd8f --- /dev/null +++ b/slices/admin/actions/auto_tagging/index.rb @@ -0,0 +1,14 @@ +module Admin + module Actions + module AutoTagging + class Index < Action + + include Deps["views.auto_tagging.index"] + + def handle(req, res) + res.render index + end + end + end + end +end \ No newline at end of file diff --git a/slices/admin/actions/auto_tagging/new.rb b/slices/admin/actions/auto_tagging/new.rb new file mode 100644 index 0000000..940ec3f --- /dev/null +++ b/slices/admin/actions/auto_tagging/new.rb @@ -0,0 +1,14 @@ +module Admin + module Actions + module AutoTagging + class New < Action + + include Deps[new_view: "views.auto_tagging.new"] + + def handle(req, res) + res.render new_view + end + end + end + end +end \ No newline at end of file diff --git a/slices/admin/commands/auto_tagging/create.rb b/slices/admin/commands/auto_tagging/create.rb new file mode 100644 index 0000000..eff337d --- /dev/null +++ b/slices/admin/commands/auto_tagging/create.rb @@ -0,0 +1,20 @@ +module Admin + module Commands + module AutoTagging + class Create + include Dry::Monads[:result] + include Deps["repos.auto_tagging_repo"] + + def call(title_contains:, body_contains:, tag_id:) + Failure() if !title_contains.empty? && !body_contains.empty? + + result = auto_tagging_repo.create(title_contains: title_contains, + body_contains: body_contains, + tag_id: tag_id) + + Success(result.id) + end + end + end + end +end diff --git a/slices/admin/commands/auto_tagging/tag.rb b/slices/admin/commands/auto_tagging/tag.rb new file mode 100644 index 0000000..f351e90 --- /dev/null +++ b/slices/admin/commands/auto_tagging/tag.rb @@ -0,0 +1,31 @@ +module Admin + module Commands + module AutoTagging + class Tag + include Dry::Monads[:result] + include Deps["repos.post_repo", "repos.auto_tagging_repo"] + + def call(auto_tag_id: nil) + auto_taggings = if auto_tag_id + auto_tagging_repo.find(auto_tag_id) + else + auto_tagging_repo.all + end + + auto_taggings.each do |auto_tagging| + posts = auto_tagging.title_only? ? + post_repo.by_title(title_contains: auto_tagging.title_contains) : + post_repo.by_content(body_contains: auto_tagging.body_contains) + + posts.each do |post| + post_repo.tag_post(post_id: post.id, + tag_id: auto_tagging.tag_id) + end + end + + Success() + end + end + end + end +end diff --git a/slices/admin/entities/auto_tagging.rb b/slices/admin/entities/auto_tagging.rb new file mode 100644 index 0000000..f15b450 --- /dev/null +++ b/slices/admin/entities/auto_tagging.rb @@ -0,0 +1,22 @@ +module Admin + module Entities + class AutoTagging < Dry::Struct + attribute :id, Types::Coercible::Integer + attribute? :title_contains, Types::Optional::String + attribute? :body_contains, Types::Optional::String + attribute :tag_id, Types::Coercible::Integer + + def title_only? + !title_contains.empty? + end + + def term + title_only? ? title_contains : body_contains + end + + class WithTag < AutoTagging + attribute :tag, Types::Tag + end + end + end +end diff --git a/slices/admin/repos/auto_tagging_repo.rb b/slices/admin/repos/auto_tagging_repo.rb new file mode 100644 index 0000000..858e707 --- /dev/null +++ b/slices/admin/repos/auto_tagging_repo.rb @@ -0,0 +1,33 @@ +module Admin + module Repos + class AutoTaggingRepo < Adamantium::Repo[:auto_taggings] + commands :create + + def find(id) + auto_taggings + .where(id: id) + .map_to(Admin::Entities::AutoTagging) + .to_a + end + + def all + auto_taggings + .map_to(Admin::Entities::AutoTagging) + .to_a + end + + def listing + auto_taggings + .combine(:tag) + .map_to(Admin::Entities::AutoTagging::WithTag) + .to_a + end + + def delete(id:) + auto_taggings + .where(id: id) + .delete + end + end + end +end diff --git a/slices/admin/repos/post_repo.rb b/slices/admin/repos/post_repo.rb new file mode 100644 index 0000000..c9b1aa5 --- /dev/null +++ b/slices/admin/repos/post_repo.rb @@ -0,0 +1,37 @@ +module Admin + module Repos + class PostRepo < Adamantium::Repo[:posts] + def tag_post(post_id:, tag_id:) + + return if posts + .post_tags + .where( + post_id: post_id, + tag_id: tag_id + ).count > 0 + + posts + .post_tags + .changeset(:create, { + post_id: post_id, + tag_id: tag_id + }) + .commit + end + + def by_title(title_contains:) + posts + .where(post_type: "post") + .published + .where(Sequel.ilike(:name, "%#{title_contains}%")).to_a + end + + def by_content(body_contains:) + posts + .where(post_type: "post") + .published + .where(Sequel.ilike(:content, "%#{body_contains}%")).to_a + end + end + end +end \ No newline at end of file diff --git a/slices/admin/repos/post_tag_repo.rb b/slices/admin/repos/post_tag_repo.rb index 1cf7485..093a99f 100644 --- a/slices/admin/repos/post_tag_repo.rb +++ b/slices/admin/repos/post_tag_repo.rb @@ -5,7 +5,6 @@ module Admin def delete(tag_id:) post_tags.where(tag_id: tag_id).delete end - end end end diff --git a/slices/admin/repos/tag_repo.rb b/slices/admin/repos/tag_repo.rb index e6a4530..86d6c0f 100644 --- a/slices/admin/repos/tag_repo.rb +++ b/slices/admin/repos/tag_repo.rb @@ -2,6 +2,11 @@ module Admin module Repos class TagRepo < Adamantium::Repo[:tags] def list + tags + .order(Sequel.function(:lower, :label)) + .to_a + end + def list_with_posts tags .combine(:posts) .order(Sequel.function(:lower, :label)) diff --git a/slices/admin/templates/auto_tagging/index.html.slim b/slices/admin/templates/auto_tagging/index.html.slim new file mode 100644 index 0000000..09ceb54 --- /dev/null +++ b/slices/admin/templates/auto_tagging/index.html.slim @@ -0,0 +1,10 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 Admin // Auto tagging + +div class="prose dark:prose-invert max-w-prose mx-auto" + - auto_taggings.each do |auto_tagging| + div id="auto-tag-#{auto_tagging.id}" + == "Tag post with #{auto_tagging.tag.label} when #{auto_tagging.title_only? ? "title" : "content"} contains #{auto_tagging.term}" + = " — " + button hx-delete="/admin/tags/auto_taggings/#{auto_tagging.id}" hx-target="#auto-tag-#{auto_tagging.id}" delete +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/slices/admin/templates/auto_tagging/new.html.slim b/slices/admin/templates/auto_tagging/new.html.slim new file mode 100644 index 0000000..4ef2926 --- /dev/null +++ b/slices/admin/templates/auto_tagging/new.html.slim @@ -0,0 +1,23 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 Admin // Auto tagging + +div class="max-w-prose mx-auto" + form method="POST" action="/admin/tags/auto_tagging" + div class="mb-4" + label class="text-gray-800 dark:text-gray-200" for="title_contains" Title contains:  + input class="text-gray-800 p-1" type="text" id="title_contains" name="title_contains" + + div class="mb-4" + label class="text-gray-800 dark:text-gray-200" for="body_contains" ... or body contains:  + input class="text-gray-800 p-1" type="text" id="body_contains" name="body_contains" + + div class="mb-4" + label class="text-gray-800 dark:text-gray-200" for="tags" Tag with:  + select class="text-gray-800" id="tags" name="tag_id" + - tags.each do |tag| + option value=tag.id + = tag.label + div class="mb-4" + button class="rounded bg-blue-100 hover:bg-blue-200 text-blue-600 px-2 hover:cursor-pointer" type="submit" + = "Create" +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/slices/admin/types.rb b/slices/admin/types.rb new file mode 100644 index 0000000..6363428 --- /dev/null +++ b/slices/admin/types.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "dry/types" + +module Admin + Types = Dry.Types + + module Types + class Tag < Dry::Struct + attribute :label, Types::Coercible::String + end + end +end \ No newline at end of file diff --git a/slices/admin/views/auto_tagging/index.rb b/slices/admin/views/auto_tagging/index.rb new file mode 100644 index 0000000..6c374a3 --- /dev/null +++ b/slices/admin/views/auto_tagging/index.rb @@ -0,0 +1,14 @@ +module Admin + module Views + module AutoTagging + class Index < Admin::View + + include Deps["repos.auto_tagging_repo"] + + expose :auto_taggings do + auto_tagging_repo.listing + end + end + end + end +end \ No newline at end of file diff --git a/slices/admin/views/auto_tagging/new.rb b/slices/admin/views/auto_tagging/new.rb new file mode 100644 index 0000000..920e474 --- /dev/null +++ b/slices/admin/views/auto_tagging/new.rb @@ -0,0 +1,14 @@ +module Admin + module Views + module AutoTagging + class New < Admin::View + + include Deps["repos.tag_repo"] + + expose :tags do + tag_repo.list.to_a + end + end + end + end +end \ No newline at end of file diff --git a/slices/admin/views/tags/index.rb b/slices/admin/views/tags/index.rb index fa245c3..5c5f473 100644 --- a/slices/admin/views/tags/index.rb +++ b/slices/admin/views/tags/index.rb @@ -6,7 +6,7 @@ module Admin include Deps["repos.tag_repo"] expose :tags do - tag_repo.list.to_a + tag_repo.list_with_posts.to_a end expose :unused_tags do |tags|