diff --git a/config/config.exs b/config/config.exs index 1596009f..609ea7c0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,17 +13,14 @@ if config_env() == :dev do end if config_env() == :test do - events = ~w(sync add_translations create_collaborator create_comment complete_review new_conflicts) - - config :accent, Accent.Hook, outbounds: [{Accent.Hook.Outbounds.Mock, events: events}] + config :accent, Accent.Hook, outbounds: [Accent.Hook.Outbounds.Mock] else config :accent, Accent.Hook, outbounds: [ - {Accent.Hook.Outbounds.Discord, events: ~w(sync complete_review new_conflicts)}, - {Accent.Hook.Outbounds.Email, events: ~w(create_collaborator create_comment)}, - {Accent.Hook.Outbounds.Slack, events: ~w(sync complete_review new_conflicts)}, - {Accent.Hook.Outbounds.Websocket, - events: ~w(sync create_collaborator create_comment complete_review new_conflicts)} + Accent.Hook.Outbounds.Discord, + Accent.Hook.Outbounds.Email, + Accent.Hook.Outbounds.Slack, + Accent.Hook.Outbounds.Websocket ] end diff --git a/lib/accent/integrations/execute/azure_storage_container.ex b/lib/accent/integrations/execute/azure_storage_container.ex index 067087e5..122fa81e 100644 --- a/lib/accent/integrations/execute/azure_storage_container.ex +++ b/lib/accent/integrations/execute/azure_storage_container.ex @@ -2,6 +2,7 @@ defmodule Accent.IntegrationManager.Execute.AzureStorageContainer do @moduledoc false alias Accent.Document + alias Accent.Hook alias Accent.Repo alias Accent.Revision alias Accent.Scopes.Document, as: DocumentScope @@ -11,12 +12,14 @@ defmodule Accent.IntegrationManager.Execute.AzureStorageContainer do alias Accent.Translation alias Accent.Version - def upload_translations(integration, params) do + def upload_translations(integration, user, params) do project = Repo.one!(Ecto.assoc(integration, :project)) version = fetch_version(project, params) documents = fetch_documents(project) revisions = fetch_revisions(project) - master_revision = Repo.preload(Repo.one!(RevisionScope.master(Ecto.assoc(project, :revisions))), :language) + + master_revision = + Repo.preload(Repo.one!(RevisionScope.master(Ecto.assoc(project, :revisions))), :language) uploads = Enum.flat_map(documents, fn document -> @@ -32,30 +35,54 @@ defmodule Accent.IntegrationManager.Execute.AzureStorageContainer do } %{render: render} = Accent.TranslationsRenderer.render_translations(render_options) - [%{document: %{document | render: render}, language: Accent.Revision.language(revision)}] + + [ + %{ + document: %{document | render: render}, + language: Accent.Revision.language(revision) + } + ] else [] end end) end) - for upload <- uploads do - file = Path.join([System.tmp_dir(), Accent.Utils.SecureRandom.urlsafe_base64(16)]) - :ok = File.write(file, upload.document.render) + uri = URI.parse(integration.data.azure_storage_container_sas) - uri = URI.parse(integration.data.azure_storage_container_sas) - extension = Accent.DocumentFormat.extension_by_format(upload.document.format) + version_tag = (version && version.tag) || "latest" - path = - Path.join([ - uri.path, - (version && version.tag) || "latest", - upload.language.slug, - upload.document.path <> "." <> extension - ]) + document_urls = + for upload <- uploads do + file = Path.join([System.tmp_dir(), Accent.Utils.SecureRandom.urlsafe_base64(16)]) + :ok = File.write(file, upload.document.render) - HTTPoison.put(URI.to_string(%{uri | path: path}), {:file, file}, [{"x-ms-blob-type", "BlockBlob"}]) - end + extension = Accent.DocumentFormat.extension_by_format(upload.document.format) + document_name = upload.document.path <> "." <> extension + + path = + Path.join([ + uri.path, + version_tag, + upload.language.slug, + document_name + ]) + + url = URI.to_string(%{uri | path: path}) + # HTTPoison.put(url, {:file, file}, [{"x-ms-blob-type", "BlockBlob"}]) + + %{name: document_name, url: url} + end + + Hook.outbound(%Hook.Context{ + event: "integration_execute_azure_storage_container", + project_id: project.id, + user_id: user.id, + payload: %{ + version_tag: version_tag, + document_urls: document_urls + } + }) :ok end diff --git a/lib/accent/integrations/integration_manager.ex b/lib/accent/integrations/integration_manager.ex index 5d7504cf..ebbe3ff4 100644 --- a/lib/accent/integrations/integration_manager.ex +++ b/lib/accent/integrations/integration_manager.ex @@ -23,7 +23,7 @@ defmodule Accent.IntegrationManager do @spec execute(Integration.t(), User.t(), map()) :: {:ok, Integration.t()} def execute(integration, user, params) do - case execute_integration(integration, params) do + case execute_integration(integration, user, params) do :ok -> integration |> change(%{last_executed_at: DateTime.utc_now(), last_executed_by_user_id: user.id}) @@ -51,16 +51,17 @@ defmodule Accent.IntegrationManager do |> validate_required([:service, :data]) end - defp execute_integration(%{service: "azure_storage_container"} = integration, params) do + defp execute_integration(%{service: "azure_storage_container"} = integration, user, params) do Accent.IntegrationManager.Execute.AzureStorageContainer.upload_translations( integration, + user, params[:azure_storage_container] ) :ok end - defp execute_integration(_integration, _params) do + defp execute_integration(_integration, _user, _params) do :noop end diff --git a/lib/graphql/types/integration.ex b/lib/graphql/types/integration.ex index 075a7f18..d844239d 100644 --- a/lib/graphql/types/integration.ex +++ b/lib/graphql/types/integration.ex @@ -15,6 +15,7 @@ defmodule Accent.GraphQL.Types.Integration do value(:complete_review, as: "complete_review") value(:create_collaborator, as: "create_collaborator") value(:create_comment, as: "create_comment") + value(:integration_execute_azure_storage_container, as: "integration_execute_azure_storage_container") end interface :project_integration do diff --git a/lib/hook/event.ex b/lib/hook/event.ex new file mode 100644 index 00000000..31919000 --- /dev/null +++ b/lib/hook/event.ex @@ -0,0 +1,6 @@ +defmodule Accent.Hook.Event do + @moduledoc false + @callback name :: String.t() + @callback triggered?(args :: map(), new_state :: map()) :: boolean() + @callback payload(args :: map(), new_state :: map()) :: map() +end diff --git a/lib/hook/events.ex b/lib/hook/events.ex new file mode 100644 index 00000000..66387168 --- /dev/null +++ b/lib/hook/events.ex @@ -0,0 +1,24 @@ +defmodule Accent.Hook.Events do + @moduledoc false + path_wildcard = Path.join(Path.dirname(__ENV__.file), "events/*.ex") + paths = Path.wildcard(path_wildcard) + paths_hash = :erlang.md5(paths) + + @callback registered_events :: [String.t()] | :all + + event_modules = + for name <- paths do + Module.safe_concat(__MODULE__, Phoenix.Naming.camelize(Path.basename(name, ".ex"))) + end + + @event_modules event_modules + + def available do + @event_modules + end + + def __mix_recompile__? do + paths = Path.wildcard(unquote(path_wildcard)) + :erlang.md5(paths) != unquote(paths_hash) + end +end diff --git a/lib/hook/events/add_translations.ex b/lib/hook/events/add_translations.ex new file mode 100644 index 00000000..21d6f070 --- /dev/null +++ b/lib/hook/events/add_translations.ex @@ -0,0 +1,23 @@ +defmodule Accent.Hook.Events.AddTranslations do + @moduledoc false + @behaviour Accent.Hook.Event + + alias Movement.Persisters.ProjectHookWorker + + @impl true + def name do + "add_translations" + end + + @impl true + def triggered?(%ProjectHookWorker.Args{} = args, _new_state) do + args.batch_action === "merge" and args.operations_count > 0 + end + + @impl true + def payload(%ProjectHookWorker.Args{} = args, _new_state) do + %{ + language_name: args.revision.language.name + } + end +end diff --git a/lib/hook/events/complete_review.ex b/lib/hook/events/complete_review.ex new file mode 100644 index 00000000..7b31ded7 --- /dev/null +++ b/lib/hook/events/complete_review.ex @@ -0,0 +1,24 @@ +defmodule Accent.Hook.Events.CompleteReview do + @moduledoc false + @behaviour Accent.Hook.Event + + alias Movement.Persisters.ProjectHookWorker + + @impl true + def name do + "complete_review" + end + + @impl true + def triggered?(%ProjectHookWorker.Args{} = args, %ProjectHookWorker.ProjectState{} = project_state) do + args.previous_project_state.reviewed_count !== args.previous_project_state.translations_count and + project_state.reviewed_count === project_state.translations_count + end + + @impl true + def payload(_args, %ProjectHookWorker.ProjectState{} = project_state) do + %{ + translations_count: project_state.translations_count + } + end +end diff --git a/lib/hook/events/new_conflicts.ex b/lib/hook/events/new_conflicts.ex new file mode 100644 index 00000000..44d6c2a2 --- /dev/null +++ b/lib/hook/events/new_conflicts.ex @@ -0,0 +1,25 @@ +defmodule Accent.Hook.Events.NewConflicts do + @moduledoc false + @behaviour Accent.Hook.Event + + alias Movement.Persisters.ProjectHookWorker + + @impl true + def name do + "new_conflicts" + end + + @impl true + def triggered?(%ProjectHookWorker.Args{} = args, %ProjectHookWorker.ProjectState{} = project_state) do + args.previous_project_state.conflicts_count < project_state.conflicts_count + end + + @impl true + def payload(%ProjectHookWorker.Args{} = args, %ProjectHookWorker.ProjectState{} = project_state) do + %{ + reviewed_count: project_state.reviewed_count, + translations_count: project_state.translations_count, + new_conflicts_count: project_state.conflicts_count - args.previous_project_state.conflicts_count + } + end +end diff --git a/lib/hook/events/sync.ex b/lib/hook/events/sync.ex new file mode 100644 index 00000000..4ec7c58d --- /dev/null +++ b/lib/hook/events/sync.ex @@ -0,0 +1,24 @@ +defmodule Accent.Hook.Events.Sync do + @moduledoc false + @behaviour Accent.Hook.Event + + alias Movement.Persisters.ProjectHookWorker + + @impl true + def name do + "sync" + end + + @impl true + def triggered?(%ProjectHookWorker.Args{} = args, _new_state) do + args.batch_action === "sync" and args.operations_count > 0 + end + + @impl true + def payload(%ProjectHookWorker.Args{} = args, _new_state) do + %{ + batch_operation_stats: args.batch_operation.stats, + document_path: args.document.path + } + end +end diff --git a/lib/hook/hook.ex b/lib/hook/hook.ex index a709cc9a..546be80c 100644 --- a/lib/hook/hook.ex +++ b/lib/hook/hook.ex @@ -1,19 +1,17 @@ defmodule Accent.Hook do @moduledoc false - def outbound(context), do: run(outbounds_modules(), context) + @outbounds_modules Application.compile_env!(:accent, __MODULE__)[:outbounds] - defp run(modules, context) do + def outbound(context) do jobs = - Enum.reduce(modules, [], fn {module, opts}, acc -> - if context.event in Keyword.fetch!(opts, :events), - do: [module.new(context) | acc], - else: acc + Enum.flat_map(@outbounds_modules, fn module -> + events = module.registered_events() + + if events === :all or context.event in events, + do: [module.new(context)], + else: [] end) Oban.insert_all(jobs) end - - defp outbounds_modules do - Application.get_env(:accent, __MODULE__)[:outbounds] - end end diff --git a/lib/hook/outbounds/discord.ex b/lib/hook/outbounds/discord.ex new file mode 100644 index 00000000..9ac796ba --- /dev/null +++ b/lib/hook/outbounds/discord.ex @@ -0,0 +1,50 @@ +defmodule Accent.Hook.Outbounds.Discord do + @moduledoc false + @behaviour Accent.Hook.Events + + use Oban.Worker, queue: :hook + + defmodule Templates do + @moduledoc false + import Accent.Hook.Outbounds.Helpers.StringTemplate + + deftemplate(:new_conflicts, """ + **<%= @user %>** just added *<%= @new_conflicts_count %> strings* to review. + The project is currently **<%= Float.round(@reviewed_count / @translations_count * 100, 2) %>** reviewed (<%= @reviewed_count %>/<%= @translations_count %>) + """) + + deftemplate(:sync, """ + **<%= @user %>** just synced a file: *<%= @document_path %>* + + **Stats:**<%= for %{"action" => action, "count" => count} <- @stats do %> + <%= Phoenix.Naming.humanize(action) %>: *<%= count %>*<% end %> + """) + + deftemplate(:complete_review, """ + **<%= @user %>** just finished reviewing all strings! + The project currently has <%= @translations_count %> reviewed translations. + """) + + deftemplate(:integration_execute_azure_storage_container, """ + **<%= @user %>** just uploaded all *<%= @version_tag %>* files to Azure Container Storage. + <%= for %{"name" => document_name, "url" => url} <- @document_urls do %> + [<%= document_name %>](<%= url %>) + <% end %> + """) + end + + @impl Accent.Hook.Events + def registered_events do + ~w(sync complete_review new_conflicts integration_execute_azure_storage_container) + end + + @impl Oban.Worker + def perform(%Oban.Job{args: args}) do + context = Accent.Hook.Context.from_worker(args) + + Accent.Hook.Outbounds.Helpers.PostURL.perform("discord", context, + http_body: &%{content: &1}, + templates: Templates + ) + end +end diff --git a/lib/hook/outbounds/discord/discord.ex b/lib/hook/outbounds/discord/discord.ex deleted file mode 100644 index 396716d8..00000000 --- a/lib/hook/outbounds/discord/discord.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Accent.Hook.Outbounds.Discord do - @moduledoc false - use Oban.Worker, queue: :hook - - @impl Oban.Worker - def perform(%Oban.Job{args: args}) do - context = Accent.Hook.Context.from_worker(args) - http_body = fn content -> %{content: content} end - - Accent.Hook.Outbounds.PostURL.perform("discord", context, - http_body: http_body, - templates: Hook.Outbounds.Discord.Templates - ) - end -end diff --git a/lib/hook/outbounds/discord/templates.ex b/lib/hook/outbounds/discord/templates.ex deleted file mode 100644 index 54976a89..00000000 --- a/lib/hook/outbounds/discord/templates.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Hook.Outbounds.Discord.Templates do - @moduledoc false - require EEx - - @sync_template """ - **<%= @user %>** just synced a file: *<%= @document_path %>* - - **Stats:**<%= for %{"action" => action, "count" => count} <- @stats do %> - <%= Phoenix.Naming.humanize(action) %>: *<%= count %>*<% end %> - """ - @new_conflicts_template """ - **<%= @user %>** just added *<%= @new_conflicts_count %> strings* to review. - The project is currently **<%= Float.round(@reviewed_count / @translations_count * 100, 2) %>** reviewed (<%= @reviewed_count %>/<%= @translations_count %>) - """ - @complete_review_template """ - **<%= @user %>** just finished reviewing all strings! - The project currently has <%= @translations_count %> reviewed translations. - """ - - EEx.function_from_string(:def, :sync, @sync_template, [:assigns], trim: true) - EEx.function_from_string(:def, :new_conflicts, @new_conflicts_template, [:assigns], trim: true) - EEx.function_from_string(:def, :complete_review, @complete_review_template, [:assigns], trim: true) -end diff --git a/lib/hook/outbounds/email.ex b/lib/hook/outbounds/email.ex index d1325039..4518d083 100644 --- a/lib/hook/outbounds/email.ex +++ b/lib/hook/outbounds/email.ex @@ -1,5 +1,7 @@ defmodule Accent.Hook.Outbounds.Email do @moduledoc false + @behaviour Accent.Hook.Events + use Oban.Worker, queue: :hook alias Accent.CreateCommentEmail @@ -7,6 +9,11 @@ defmodule Accent.Hook.Outbounds.Email do alias Accent.Repo alias Accent.Translation + @impl Accent.Hook.Events + def registered_events do + ~w(create_collaborator create_comment) + end + @impl Oban.Worker def perform(%Oban.Job{args: args}) do context = Accent.Hook.Context.from_worker(args) diff --git a/lib/hook/outbounds/post_url.ex b/lib/hook/outbounds/helpers/post_url.ex similarity index 61% rename from lib/hook/outbounds/post_url.ex rename to lib/hook/outbounds/helpers/post_url.ex index 5907245c..3013d239 100644 --- a/lib/hook/outbounds/post_url.ex +++ b/lib/hook/outbounds/helpers/post_url.ex @@ -1,4 +1,4 @@ -defmodule Accent.Hook.Outbounds.PostURL do +defmodule Accent.Hook.Outbounds.Helpers.PostURL do @moduledoc false import Ecto.Query, only: [where: 2] @@ -58,27 +58,35 @@ defmodule Accent.Hook.Outbounds.PostURL do defp formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] defp formatted_diff(diff), do: [Integer.to_string(diff), "µs"] - defp build_content(templates, %{event: "sync", user: user, payload: payload}) do + defp build_content(templates, %{event: "sync"} = context) do templates.sync(%{ - user: User.name_with_fallback(user), - document_path: payload["document_path"], - stats: payload["batch_operation_stats"] + user: User.name_with_fallback(context.user), + document_path: context.payload["document_path"], + stats: context.payload["batch_operation_stats"] }) end - defp build_content(templates, %{event: "new_conflicts", user: user, payload: payload}) do + defp build_content(templates, %{event: "new_conflicts"} = context) do templates.new_conflicts(%{ - user: User.name_with_fallback(user), - reviewed_count: payload["reviewed_count"], - new_conflicts_count: payload["new_conflicts_count"], - translations_count: payload["translations_count"] + user: User.name_with_fallback(context.user), + reviewed_count: context.payload["reviewed_count"], + new_conflicts_count: context.payload["new_conflicts_count"], + translations_count: context.payload["translations_count"] }) end - defp build_content(templates, %{event: "complete_review", user: user, payload: payload}) do + defp build_content(templates, %{event: "complete_review"} = context) do templates.complete_review(%{ - user: User.name_with_fallback(user), - translations_count: payload["translations_count"] + user: User.name_with_fallback(context.user), + translations_count: context.payload["translations_count"] + }) + end + + defp build_content(templates, %{event: "integration_execute_azure_storage_container"} = context) do + templates.integration_execute_azure_storage_container(%{ + user: User.name_with_fallback(context.user), + version_tag: context.payload["version_tag"], + document_urls: context.payload["document_urls"] }) end end diff --git a/lib/hook/outbounds/helpers/string_template.ex b/lib/hook/outbounds/helpers/string_template.ex new file mode 100644 index 00000000..36b7823c --- /dev/null +++ b/lib/hook/outbounds/helpers/string_template.ex @@ -0,0 +1,11 @@ +defmodule Accent.Hook.Outbounds.Helpers.StringTemplate do + @moduledoc false + + defmacro deftemplate(name, template) do + quote do + require EEx + + EEx.function_from_string(:def, unquote(name), unquote(template), [:assigns], trim: true) + end + end +end diff --git a/lib/hook/outbounds/slack.ex b/lib/hook/outbounds/slack.ex new file mode 100644 index 00000000..5d8229f4 --- /dev/null +++ b/lib/hook/outbounds/slack.ex @@ -0,0 +1,50 @@ +defmodule Accent.Hook.Outbounds.Slack do + @moduledoc false + @behaviour Accent.Hook.Events + + use Oban.Worker, queue: :hook + + defmodule Templates do + @moduledoc false + import Accent.Hook.Outbounds.Helpers.StringTemplate + + deftemplate(:new_conflicts, """ + *<%= @user %>* just added _<%= @new_conflicts_count %> strings_ to review. + The project is currently *<%= Float.round(@reviewed_count / @translations_count * 100, 2) %>* reviewed (<%= @reviewed_count %>/<%= @translations_count %>) + """) + + deftemplate(:sync, """ + *<%= @user %>* just synced a file: _<%= @document_path %>_ + + *Stats:*<%= for %{"action" => action, "count" => count} <- @stats do %> + <%= Phoenix.Naming.humanize(action) %>: _<%= count %>_<% end %> + """) + + deftemplate(:complete_review, """ + *<%= @user %>* just finished reviewing all strings! + The project currently has <%= @translations_count %> reviewed translations. + """) + + deftemplate(:integration_execute_azure_storage_container, """ + *<%= @user %>* just uploaded all _<%= @version_tag %>_ files to Azure Container Storage. + <%= for %{"name" => document_name, "url" => url} <- @document_urls do %> + [<%= document_name %>](<%= url %>) + <% end %> + """) + end + + @impl Accent.Hook.Events + def registered_events do + ~w(sync complete_review new_conflicts integration_execute_azure_storage_container) + end + + @impl Oban.Worker + def perform(%Oban.Job{args: args}) do + context = Accent.Hook.Context.from_worker(args) + + Accent.Hook.Outbounds.Helpers.PostURL.perform("slack", context, + http_body: &%{text: &1}, + templates: Templates + ) + end +end diff --git a/lib/hook/outbounds/slack/slack.ex b/lib/hook/outbounds/slack/slack.ex deleted file mode 100644 index 03da4307..00000000 --- a/lib/hook/outbounds/slack/slack.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Accent.Hook.Outbounds.Slack do - @moduledoc false - use Oban.Worker, queue: :hook - - @impl Oban.Worker - def perform(%Oban.Job{args: args}) do - context = Accent.Hook.Context.from_worker(args) - http_body = fn content -> %{text: content} end - - Accent.Hook.Outbounds.PostURL.perform("slack", context, - http_body: http_body, - templates: Hook.Outbounds.Slack.Templates - ) - end -end diff --git a/lib/hook/outbounds/slack/templates.ex b/lib/hook/outbounds/slack/templates.ex deleted file mode 100644 index a21a927c..00000000 --- a/lib/hook/outbounds/slack/templates.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Hook.Outbounds.Slack.Templates do - @moduledoc false - require EEx - - @sync_template """ - *<%= @user %>* just synced a file: _<%= @document_path %>_ - - *Stats:*<%= for %{"action" => action, "count" => count} <- @stats do %> - <%= Phoenix.Naming.humanize(action) %>: _<%= count %>_<% end %> - """ - @new_conflicts_template """ - *<%= @user %>* just added _<%= @new_conflicts_count %> strings_ to review. - The project is currently *<%= Float.round(@reviewed_count / @translations_count * 100, 2) %>* reviewed (<%= @reviewed_count %>/<%= @translations_count %>) - """ - @complete_review_template """ - *<%= @user %>* just finished reviewing all strings! - The project currently has <%= @translations_count %> reviewed translations. - """ - - EEx.function_from_string(:def, :sync, @sync_template, [:assigns], trim: true) - EEx.function_from_string(:def, :new_conflicts, @new_conflicts_template, [:assigns], trim: true) - EEx.function_from_string(:def, :complete_review, @complete_review_template, [:assigns], trim: true) -end diff --git a/lib/hook/outbounds/websocket.ex b/lib/hook/outbounds/websocket.ex index f70c7688..749bb503 100644 --- a/lib/hook/outbounds/websocket.ex +++ b/lib/hook/outbounds/websocket.ex @@ -1,7 +1,14 @@ defmodule Accent.Hook.Outbounds.Websocket do @moduledoc false + @behaviour Accent.Hook.Events + use Oban.Worker, queue: :hook + @impl Accent.Hook.Events + def registered_events do + ~w(sync create_collaborator create_comment complete_review new_conflicts) + end + @impl Oban.Worker def perform(%Oban.Job{args: args}) do args diff --git a/lib/movement/persisters/base.ex b/lib/movement/persisters/base.ex index 910a6b8c..a8c71321 100644 --- a/lib/movement/persisters/base.ex +++ b/lib/movement/persisters/base.ex @@ -4,7 +4,7 @@ defmodule Movement.Persisters.Base do alias Accent.Repo alias Movement.Mappers.OperationsStats, as: StatMapper alias Movement.Migrator - alias Movement.Persisters.ProjectStateChangeWorker + alias Movement.Persisters.ProjectHookWorker require Ecto.Query @@ -39,7 +39,9 @@ defmodule Movement.Persisters.Base do end def execute(context) do - project_state_change_context = %{ + project_context = %{ + batch_action: context.assigns[:batch_action], + operations_count: Enum.count(context.operations), project_id: context.assigns[:project] && context.assigns.project.id, document_id: context.assigns[:document] && context.assigns.document.id, master_revision_id: context.assigns[:master_revision] && context.assigns.master_revision.id, @@ -47,15 +49,14 @@ defmodule Movement.Persisters.Base do version_id: context.assigns[:version] && context.assigns.version.id, batch_operation_id: context.assigns[:batch_operation] && context.assigns.batch_operation.id, user_id: context.assigns[:user_id], - previous_project_state: ProjectStateChangeWorker.get_project_state(context.assigns[:project]) + previous_project_state: ProjectHookWorker.get_project_state(context.assigns[:project]) } context |> persist_operations() |> migrate_up_operations() |> tap(fn _ -> - project_state_change_context.previous_project_state && - Oban.insert(ProjectStateChangeWorker.new(project_state_change_context)) + context.assigns[:project] && Oban.insert(ProjectHookWorker.new(project_context)) end) end diff --git a/lib/movement/persisters/project_hook_worker.ex b/lib/movement/persisters/project_hook_worker.ex new file mode 100644 index 00000000..cca2107a --- /dev/null +++ b/lib/movement/persisters/project_hook_worker.ex @@ -0,0 +1,92 @@ +defmodule Movement.Persisters.ProjectHookWorker do + @moduledoc false + use Oban.Worker, queue: :hook + + import Ecto.Query + + alias Accent.Hook + alias Accent.Repo + alias Accent.Scopes.Project, as: ProjectScope + + defmodule ProjectState do + @moduledoc false + @derive Jason.Encoder + defstruct translations_count: 0, reviewed_count: 0, conflicts_count: 0 + + @type t :: %__MODULE__{} + end + + defmodule Args do + @moduledoc false + defstruct previous_project_state: %ProjectState{}, + operations_count: 0, + project: nil, + document: nil, + master_revision: nil, + revision: nil, + version: nil, + batch_operation: nil, + batch_action: nil, + user: nil + + @type t :: %__MODULE__{} + end + + @impl Oban.Worker + def perform(%Oban.Job{args: args}) do + args = cast_args(args) + current_project_state = get_project_state(args.project) + + for module <- Accent.Hook.Events.available() do + if module.triggered?(args, current_project_state) do + Hook.outbound(%Hook.Context{ + event: module.name(), + project_id: args.project.id, + user_id: args.user && args.user.id, + payload: module.payload(args, current_project_state) + }) + end + end + + :ok + end + + def get_project_state(nil), do: nil + + def get_project_state(project) do + project = + Accent.Project + |> from(where: [id: ^project.id]) + |> ProjectScope.with_stats() + |> Repo.one() + + struct!(ProjectState, Map.take(project, ~w(translations_count reviewed_count conflicts_count)a)) + end + + defp cast_args(args) do + %Args{ + previous_project_state: cast_project_state(args["previous_project_state"]), + project: get_record(Accent.Project, args["project_id"]), + document: get_record(Accent.Document, args["document_id"]), + master_revision: get_record(Accent.Revision, args["master_revision_id"]), + revision: get_record(Accent.Revision, args["revision_id"]), + version: get_record(Accent.Version, args["version_id"]), + batch_operation: get_record(Accent.Operation, args["batch_operation_id"]), + batch_action: args["batch_action"], + user: get_record(Accent.User, args["user_id"]) + } + end + + defp cast_project_state(nil), do: nil + + defp cast_project_state(args) do + %ProjectState{ + translations_count: args["translations_count"], + reviewed_count: args["reviewed_count"], + conflicts_count: args["conflicts_count"] + } + end + + defp get_record(_schema, nil), do: nil + defp get_record(schema, id), do: Repo.get(schema, id) +end diff --git a/lib/movement/persisters/project_state_change_worker.ex b/lib/movement/persisters/project_state_change_worker.ex deleted file mode 100644 index edc9e99f..00000000 --- a/lib/movement/persisters/project_state_change_worker.ex +++ /dev/null @@ -1,90 +0,0 @@ -defmodule Movement.Persisters.ProjectStateChangeWorker do - @moduledoc false - use Oban.Worker, queue: :hook - - import Ecto.Query - - alias Accent.Hook - alias Accent.Repo - alias Accent.Scopes.Project, as: ProjectScope - - @impl Oban.Worker - def perform(%Oban.Job{args: args}) do - args = cast_args(args) - current_project_state = get_project_state(args.project) - - if new_conflicts_to_review?(args.previous_project_state, current_project_state) do - Hook.outbound(%Hook.Context{ - event: "new_conflicts", - project_id: args.project.id, - user_id: args.user.id, - payload: %{ - reviewed_count: current_project_state.project.reviewed_count, - translations_count: current_project_state.project.translations_count, - new_conflicts_count: - current_project_state.project.conflicts_count - args.previous_project_state.project.conflicts_count - } - }) - end - - if all_reviewed?(args.previous_project_state, current_project_state) do - Hook.outbound(%Hook.Context{ - event: "complete_review", - project_id: args.project.id, - user_id: args.user.id, - payload: %{ - translations_count: current_project_state.project.translations_count - } - }) - end - - :ok - end - - defp new_conflicts_to_review?(previous_state, current_state) do - previous_state.project.conflicts_count < current_state.project.conflicts_count - end - - defp all_reviewed?(previous_state, current_state) do - previous_state.project.reviewed_count !== previous_state.project.translations_count and - current_state.project.reviewed_count === current_state.project.translations_count - end - - def get_project_state(nil), do: nil - - def get_project_state(project) do - project = - Accent.Project - |> from(where: [id: ^project.id]) - |> ProjectScope.with_stats() - |> Repo.one() - - %{project: Map.take(project, ~w(translations_count reviewed_count conflicts_count)a)} - end - - defp cast_project_state(args) do - %{ - project: %{ - translations_count: args["project"]["translations_count"], - reviewed_count: args["project"]["reviewed_count"], - conflicts_count: args["project"]["conflicts_count"] - } - } - end - - defp cast_args(args) do - %{ - previous_project_state: cast_project_state(args["previous_project_state"]), - project: get_record(Accent.Project, args["project_id"]), - document: get_record(Accent.Document, args["document_id"]), - master_revision: get_record(Accent.Revision, args["master_revision_id"]), - revision: get_record(Accent.Revision, args["revision_id"]), - version: get_record(Accent.Version, args["version_id"]), - batch_operation: get_record(Accent.Operation, args["batch_operation_id"]), - user: get_record(Accent.User, args["user_id"]) - } - end - - defp get_record(_schema, nil), do: nil - defp get_record(schema, id), do: Repo.get(schema, id) -end diff --git a/lib/web/controllers/merge_controller.ex b/lib/web/controllers/merge_controller.ex index 64541363..a856ab8e 100644 --- a/lib/web/controllers/merge_controller.ex +++ b/lib/web/controllers/merge_controller.ex @@ -3,9 +3,9 @@ defmodule Accent.MergeController do import Canary.Plugs - alias Accent.Hook.Context, as: HookContext alias Accent.Project alias Movement.Builders.RevisionMerge, as: RevisionMergeBuilder + alias Movement.Context alias Movement.Persisters.RevisionMerge, as: RevisionMergePersister plug(Plug.Assign, canary_action: :merge) @@ -44,27 +44,14 @@ defmodule Accent.MergeController do """ def create(conn, _params) do conn.assigns[:movement_context] - |> Movement.Context.assign(:revision, conn.assigns[:revision]) - |> Movement.Context.assign(:merge_type, conn.assigns[:merge_type]) - |> Movement.Context.assign(:options, conn.assigns[:merge_options]) - |> Movement.Context.assign(:user_id, conn.assigns[:current_user].id) + |> Context.assign(:revision, conn.assigns[:revision]) + |> Context.assign(:merge_type, conn.assigns[:merge_type]) + |> Context.assign(:options, conn.assigns[:merge_options]) + |> Context.assign(:user_id, conn.assigns[:current_user].id) |> RevisionMergeBuilder.build() |> RevisionMergePersister.persist() |> case do - {:ok, {_context, []}} -> - send_resp(conn, :ok, "") - - {:ok, _} -> - Accent.Hook.outbound(%HookContext{ - event: "add_translations", - project_id: conn.assigns[:project].id, - user_id: conn.assigns[:current_user].id, - payload: %{ - merge_type: conn.assigns[:merge_type], - language_name: conn.assigns[:revision].language.name - } - }) - + {:ok, {_context, _}} -> send_resp(conn, :ok, "") {:error, _reason} -> @@ -74,7 +61,7 @@ defmodule Accent.MergeController do defp assign_comparer(conn, _) do comparer = Movement.Comparer.comparer(:merge, conn.params["merge_type"]) - context = Movement.Context.assign(conn.assigns[:movement_context], :comparer, comparer) + context = Context.assign(conn.assigns[:movement_context], :comparer, comparer) assign(conn, :movement_context, context) end diff --git a/lib/web/controllers/sync_controller.ex b/lib/web/controllers/sync_controller.ex index a110745a..e0310072 100644 --- a/lib/web/controllers/sync_controller.ex +++ b/lib/web/controllers/sync_controller.ex @@ -3,9 +3,9 @@ defmodule Accent.SyncController do import Canary.Plugs - alias Accent.Hook.Context, as: HookContext alias Accent.Project alias Movement.Builders.ProjectSync, as: SyncBuilder + alias Movement.Context alias Movement.Persisters.ProjectSync, as: SyncPersister plug(Plug.Assign, canary_action: :sync) @@ -41,25 +41,12 @@ defmodule Accent.SyncController do """ def create(conn, _) do conn.assigns[:movement_context] - |> Movement.Context.assign(:project, conn.assigns[:project]) - |> Movement.Context.assign(:user_id, conn.assigns[:current_user].id) + |> Context.assign(:project, conn.assigns[:project]) + |> Context.assign(:user_id, conn.assigns[:current_user].id) |> SyncBuilder.build() |> SyncPersister.persist() |> case do - {:ok, {_context, []}} -> - send_resp(conn, :ok, "") - - {:ok, {context, _operations}} -> - Accent.Hook.outbound(%HookContext{ - event: "sync", - project_id: conn.assigns[:project].id, - user_id: conn.assigns[:current_user].id, - payload: %{ - batch_operation_stats: context.assigns[:batch_operation].stats, - document_path: context.assigns[:document].path - } - }) - + {:ok, {_context, _}} -> send_resp(conn, :ok, "") {:error, _reason} -> @@ -69,7 +56,7 @@ defmodule Accent.SyncController do defp assign_comparer(conn, _) do comparer = Movement.Comparer.comparer(:sync, conn.params["sync_type"]) - context = Movement.Context.assign(conn.assigns[:movement_context], :comparer, comparer) + context = Context.assign(conn.assigns[:movement_context], :comparer, comparer) assign(conn, :movement_context, context) end diff --git a/test/hook/hook_test.exs b/test/hook/hook_test.exs index 31a82b2f..63684b2c 100644 --- a/test/hook/hook_test.exs +++ b/test/hook/hook_test.exs @@ -31,13 +31,4 @@ defmodule AccentTest.Hook do args: %{worker_args | "event" => "sync"} ) end - - test "unsupported event", %{context: context, worker_args: worker_args} do - Hook.outbound(%{context | event: "foobar"}) - - refute_enqueued( - worker: Hook.Outbounds.Mock, - args: %{worker_args | "event" => "foobar"} - ) - end end diff --git a/test/movement/persisters/project_state_change_worker_test.exs b/test/movement/persisters/project_hook_worker_test.exs similarity index 82% rename from test/movement/persisters/project_state_change_worker_test.exs rename to test/movement/persisters/project_hook_worker_test.exs index f4f1a977..c90656c0 100644 --- a/test/movement/persisters/project_state_change_worker_test.exs +++ b/test/movement/persisters/project_hook_worker_test.exs @@ -1,4 +1,4 @@ -defmodule Movement.Persisters.ProjectStateChangeWorkerTest do +defmodule Movement.Persisters.ProjectHookWorkerTest do use Accent.RepoCase, async: true alias Accent.Document @@ -7,7 +7,7 @@ defmodule Movement.Persisters.ProjectStateChangeWorkerTest do alias Accent.Repo alias Accent.Translation alias Accent.User - alias Movement.Persisters.ProjectStateChangeWorker, as: Worker + alias Movement.Persisters.ProjectHookWorker, as: Worker setup do user = Repo.insert!(%User{email: "test@test.com"}) @@ -26,11 +26,9 @@ defmodule Movement.Persisters.ProjectStateChangeWorkerTest do args = %{ "project_id" => project.id, "previous_project_state" => %{ - "project" => %{ - "translations_count" => 0, - "reviewed_count" => 0, - "conflicts_count" => 0 - } + "translations_count" => 0, + "reviewed_count" => 0, + "conflicts_count" => 0 } } @@ -53,11 +51,9 @@ defmodule Movement.Persisters.ProjectStateChangeWorkerTest do "project_id" => project.id, "user_id" => user.id, "previous_project_state" => %{ - "project" => %{ - "translations_count" => 0, - "reviewed_count" => 0, - "conflicts_count" => 0 - } + "translations_count" => 0, + "reviewed_count" => 0, + "conflicts_count" => 0 } } @@ -92,11 +88,9 @@ defmodule Movement.Persisters.ProjectStateChangeWorkerTest do "project_id" => project.id, "user_id" => user.id, "previous_project_state" => %{ - "project" => %{ - "translations_count" => 1, - "reviewed_count" => 0, - "conflicts_count" => 1 - } + "translations_count" => 1, + "reviewed_count" => 0, + "conflicts_count" => 1 } } diff --git a/test/support/mocks.ex b/test/support/mocks.ex index e70aba49..516b67af 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -1,7 +1,14 @@ defmodule Accent.Hook.Outbounds.Mock do @moduledoc false + @behaviour Accent.Hook.Events + use Oban.Worker, queue: :hook + @impl Accent.Hook.Events + def registered_events do + :all + end + @impl Oban.Worker def perform(_job) do :ok diff --git a/test/web/controllers/merge_controller_test.exs b/test/web/controllers/merge_controller_test.exs index 290e8eb2..fc319615 100644 --- a/test/web/controllers/merge_controller_test.exs +++ b/test/web/controllers/merge_controller_test.exs @@ -66,15 +66,7 @@ defmodule AccentTest.MergeController do assert response.status == 200 - assert_enqueued( - worker: Accent.Hook.Outbounds.Mock, - args: %{ - "event" => "add_translations", - "payload" => %{"language_name" => "french", "merge_type" => nil}, - "project_id" => project.id, - "user_id" => user.id - } - ) + assert_enqueued(worker: Movement.Persisters.ProjectHookWorker) merge_on_proposed_operation = Repo.one(from(o in Operation, where: [action: ^"merge_on_proposed"])) merge_operation = Repo.one(from(o in Operation, where: [action: ^"merge"])) @@ -160,15 +152,7 @@ defmodule AccentTest.MergeController do assert response.status == 200 - assert_enqueued( - worker: Accent.Hook.Outbounds.Mock, - args: %{ - "event" => "add_translations", - "payload" => %{"language_name" => "french", "merge_type" => nil}, - "project_id" => project.id, - "user_id" => user.id - } - ) + assert_enqueued(worker: Movement.Persisters.ProjectHookWorker) merge_on_corrected_force_operation = Repo.one(from(o in Operation, where: [action: ^"merge_on_corrected_force"])) merge_operation = Repo.one(from(o in Operation, where: [action: ^"merge"])) @@ -213,15 +197,7 @@ defmodule AccentTest.MergeController do assert response.status == 200 - assert_enqueued( - worker: Accent.Hook.Outbounds.Mock, - args: %{ - "event" => "add_translations", - "payload" => %{"language_name" => "french", "merge_type" => nil}, - "project_id" => project.id, - "user_id" => user.id - } - ) + assert_enqueued(worker: Movement.Persisters.ProjectHookWorker) merge_on_proposed_operation = Repo.one(from(o in Operation, where: [action: ^"merge_on_proposed"])) merge_operation = Repo.one(from(o in Operation, where: [action: ^"merge"])) diff --git a/test/web/controllers/sync_controller_test.exs b/test/web/controllers/sync_controller_test.exs index c56bdb81..c4095078 100644 --- a/test/web/controllers/sync_controller_test.exs +++ b/test/web/controllers/sync_controller_test.exs @@ -52,18 +52,7 @@ defmodule AccentTest.SyncController do assert response.status == 200 - assert_enqueued( - worker: Accent.Hook.Outbounds.Mock, - args: %{ - "event" => "sync", - "payload" => %{ - "batch_operation_stats" => [%{"action" => "new", "count" => 3}], - "document_path" => "simple" - }, - "project_id" => project.id, - "user_id" => user.id - } - ) + assert_enqueued(worker: Movement.Persisters.ProjectHookWorker) assert Enum.map(Repo.all(Document), &Map.get(&1, :path)) == ["simple"] diff --git a/webapp/app/components/conflicts-list/component.ts b/webapp/app/components/conflicts-list/component.ts index 5b985816..f8688228 100644 --- a/webapp/app/components/conflicts-list/component.ts +++ b/webapp/app/components/conflicts-list/component.ts @@ -51,10 +51,6 @@ export default class ConflictsList extends Component { ); } - get showRevisionsHeader() { - return this.revisions.length > 1; - } - @action handleFocus(id: string) { this.selectedTranslationId = id; diff --git a/webapp/app/components/project-settings/integrations/form/data-control-checkboxes/component.ts b/webapp/app/components/project-settings/integrations/form/data-control-checkboxes/component.ts index 569edd7b..bd7538e7 100644 --- a/webapp/app/components/project-settings/integrations/form/data-control-checkboxes/component.ts +++ b/webapp/app/components/project-settings/integrations/form/data-control-checkboxes/component.ts @@ -29,6 +29,11 @@ export default class DataControlCheckboxes extends Component { label: 'components.project_settings.integrations.events.options.complete_review', }, + { + value: 'INTEGRATION_EXECUTE_AZURE_STORAGE_CONTAINER', + label: + 'components.project_settings.integrations.events.options.integration_execute_azure_storage_container', + }, ]; @tracked diff --git a/webapp/app/locales/en-us.json b/webapp/app/locales/en-us.json index 627b0642..9499b357 100644 --- a/webapp/app/locales/en-us.json +++ b/webapp/app/locales/en-us.json @@ -532,7 +532,8 @@ "options": { "sync": "Sync with any changes", "new_conflicts": "New strings to review", - "complete_review": "Project is 100% reviewed" + "complete_review": "Project is 100% reviewed", + "integration_execute_azure_storage_container": "Files uploaded to Azure Storage Container" } }, "execute": { diff --git a/webapp/app/locales/fr-ca.json b/webapp/app/locales/fr-ca.json index a10216da..8db05878 100644 --- a/webapp/app/locales/fr-ca.json +++ b/webapp/app/locales/fr-ca.json @@ -549,7 +549,8 @@ "options": { "sync": "Synchroniser avec n’importe quelles modifications", "new_conflicts": "Nouvelles chaîne à réviser", - "complete_review": "Le projet est revu à 100%" + "complete_review": "Le projet est revu à 100%", + "integration_execute_azure_storage_container": "Fichiers téléversés sur Azure Storage Container" } }, "execute": { diff --git a/webapp/app/services/apollo-mutate.ts b/webapp/app/services/apollo-mutate.ts index c2608005..3912cb9d 100644 --- a/webapp/app/services/apollo-mutate.ts +++ b/webapp/app/services/apollo-mutate.ts @@ -15,11 +15,7 @@ export default class ApolloMutate extends Service { const operationName = Object.keys(data)[0]; if (!data[operationName]?.errors?.length) { - const updatedData = { - data, - [operationName]: {...data[operationName], errors: null}, - }; - return updatedData; + return {...data[operationName], errors: null}; } return data[operationName]; diff --git a/webapp/app/templates/components/conflicts-list.hbs b/webapp/app/templates/components/conflicts-list.hbs index 59ed0880..a0df5326 100644 --- a/webapp/app/templates/components/conflicts-list.hbs +++ b/webapp/app/templates/components/conflicts-list.hbs @@ -6,18 +6,16 @@ {{/if}} -{{#if this.showRevisionsHeader}} - -{{/if}} +