Refactor hook to unify in movement persister module

This commit is contained in:
Simon Prévost 2024-02-21 13:24:23 -05:00
parent d718fc0020
commit 88789980fc
37 changed files with 486 additions and 348 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

6
lib/hook/event.ex Normal file
View File

@ -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

24
lib/hook/events.ex Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

24
lib/hook/events/sync.ex Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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"]))

View File

@ -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"]

View File

@ -51,10 +51,6 @@ export default class ConflictsList extends Component<Args> {
);
}
get showRevisionsHeader() {
return this.revisions.length > 1;
}
@action
handleFocus(id: string) {
this.selectedTranslationId = id;

View File

@ -29,6 +29,11 @@ export default class DataControlCheckboxes extends Component<Args> {
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

View File

@ -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": {

View File

@ -549,7 +549,8 @@
"options": {
"sync": "Synchroniser avec nimporte 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": {

View File

@ -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];

View File

@ -6,18 +6,16 @@
</div>
{{/if}}
{{#if this.showRevisionsHeader}}
<ul local-class='conflicts-header' style='--group-columns-count: {{this.revisions.length}};'>
{{#each this.mappedRevisions as |revision|}}
<li local-class='conflicts-header-item'>
{{revision.name}}
<span local-class='conflicts-header-item-slug'>
{{revision.slug}}
</span>
</li>
{{/each}}
</ul>
{{/if}}
<ul local-class='conflicts-header' style='--group-columns-count: {{this.revisions.length}};'>
{{#each this.mappedRevisions as |revision|}}
<li local-class='conflicts-header-item'>
{{revision.name}}
<span local-class='conflicts-header-item-slug'>
{{revision.slug}}
</span>
</li>
{{/each}}
</ul>
<ul local-class='conflicts-items' style='--group-columns-count: {{this.revisions.length}};'>
{{#each @groupedTranslations key='id' as |groupedTranslation index|}}