lib/step_flow/controllers/workflow_controller.ex

defmodule StepFlow.WorkflowController do
  use StepFlow, :controller

  require Logger

  alias StepFlow.Configuration
  alias StepFlow.Controller.Helpers
  alias StepFlow.Metrics.WorkflowInstrumenter
  alias StepFlow.Repo
  alias StepFlow.Step
  alias StepFlow.Workflows
  alias StepFlow.Workflows.Workflow

  action_fallback(StepFlow.FallbackController)

  def index(%Plug.Conn{assigns: %{current_user: user}} = conn, %{"job_id" => job_id} = params) do
    mode = StepFlow.Map.get_by_key_or_atom(params, :mode, "full")

    workflow =
      Workflows.get_workflow_for_job!(job_id)
      |> Repo.preload([:jobs, :status])

    if Helpers.has_right?("workflow::" <> workflow.identifier, user, "view") do
      conn
      |> put_view(StepFlow.WorkflowView)
      |> render("show.json", workflow: workflow, mode: mode)
    else
      conn
      |> put_status(:forbidden)
      |> put_view(StepFlow.WorkflowDefinitionView)
      |> render("error.json",
        errors: %{reason: "Forbidden to view workflow with this identifier"}
      )
    end
  end

  def index(%Plug.Conn{assigns: %{current_user: user}} = conn, params) do
    workflows =
      params
      |> Map.put("roles", user.roles)
      |> Workflows.list_workflows()

    conn
    |> put_view(StepFlow.WorkflowView)
    |> render("index.json", workflows: workflows)
  end

  def index(conn, _) do
    conn
    |> put_status(:forbidden)
    |> put_view(StepFlow.WorkflowDefinitionView)
    |> render("error.json",
      errors: %{reason: "Forbidden to view workflows."}
    )
  end

  def create_workflow(%Plug.Conn{assigns: %{current_user: user}} = conn, workflow_params) do
    if Map.has_key?(user, :uuid) do
      workflow_params = Map.put(workflow_params, :user_uuid, user.uuid)

      case Workflows.create_workflow(workflow_params) do
        {:ok, %Workflow{} = workflow} ->
          WorkflowInstrumenter.inc(:step_flow_workflows_created, workflow.identifier)
          Workflows.Status.define_workflow_status(workflow.id, :created_workflow)
          Step.start_next(workflow)

          StepFlow.Notification.send("new_workflow", %{workflow_id: workflow.id})

          conn
          |> put_status(:created)
          |> put_view(StepFlow.WorkflowView)
          |> render("created.json", workflow: workflow)

        {:error, changeset} ->
          conn
          |> put_status(:unprocessable_entity)
          |> put_view(StepFlow.ChangesetView)
          |> render("error.json", changeset: changeset)
      end
    else
      conn
      |> put_status(:forbidden)
      |> put_view(StepFlow.WorkflowDefinitionView)
      |> render("error.json",
        errors: %{reason: "Forbidden to create workflow with a user with no uuid"}
      )
    end
  end

  def locate_and_create_workflow(conn, user, workflow_definition, workflow_params) do
    case workflow_definition do
      nil ->
        conn
        |> put_status(:unprocessable_entity)
        |> put_view(StepFlow.WorkflowDefinitionView)
        |> render("error.json",
          errors: %{reason: "Unable to locate workflow with this identifier"}
        )

      workflow_definition ->
        if Helpers.has_right?("workflow::" <> workflow_definition.identifier, user, "create") do
          workflow_description =
            workflow_definition
            |> Map.put(:reference, Map.get(workflow_params, "reference"))
            |> Map.put(
              :parameters,
              merge_parameters(
                StepFlow.Map.get_by_key_or_atom(workflow_definition, :parameters),
                Map.get(workflow_params, "parameters", %{})
              )
            )
            |> Map.from_struct()

          create_workflow(conn, workflow_description)
        else
          conn
          |> put_status(:forbidden)
          |> put_view(StepFlow.WorkflowDefinitionView)
          |> render("error.json",
            errors: %{reason: "Forbidden to create workflow with this identifier"}
          )
        end
    end
  end

  def create(
        %Plug.Conn{assigns: %{current_user: user}} = conn,
        %{
          "workflow_identifier" => identifier,
          "version_major" => version_major,
          "version_minor" => version_minor,
          "version_micro" => version_micro
        } = workflow_params
      ) do
    workflow_definition =
      StepFlow.WorkflowDefinitions.get_workflow_definition(
        identifier,
        version_major,
        version_minor,
        version_micro
      )

    locate_and_create_workflow(conn, user, workflow_definition, workflow_params)
  end

  def create(
        %Plug.Conn{assigns: %{current_user: user}} = conn,
        %{"workflow_identifier" => identifier} = workflow_params
      ) do
    workflow_definition = StepFlow.WorkflowDefinitions.get_workflow_definition(identifier)
    locate_and_create_workflow(conn, user, workflow_definition, workflow_params)
  end

  def create(conn, _workflow_params) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(StepFlow.WorkflowDefinitionView)
    |> render("error.json",
      errors: %{reason: "Missing Workflow identifier parameter"}
    )
  end

  defp merge_parameters(parameters, request_parameters, result \\ [])
  defp merge_parameters([], _request_parameters, result), do: result

  defp merge_parameters([parameter | tail], request_parameters, result) do
    result =
      case Map.get(request_parameters, Map.get(parameter, "id")) do
        nil ->
          List.insert_at(result, -1, parameter)

        parameter_value ->
          List.insert_at(result, -1, Map.put(parameter, "value", parameter_value))
      end

    merge_parameters(tail, request_parameters, result)
  end

  def show(%Plug.Conn{assigns: %{current_user: user}} = conn, %{"id" => id} = params) do
    mode = StepFlow.Map.get_by_key_or_atom(params, :mode, "full")

    workflow =
      Workflows.get_workflow!(id)
      |> Repo.preload([:jobs, :status])

    if Helpers.has_right?("workflow::" <> workflow.identifier, user, "view") do
      conn
      |> put_view(StepFlow.WorkflowView)
      |> render("show.json", workflow: workflow, mode: mode)
    else
      conn
      |> put_status(:forbidden)
      |> put_view(StepFlow.WorkflowDefinitionView)
      |> render("error.json",
        errors: %{reason: "Forbidden to view workflow with this identifier"}
      )
    end
  end

  def show(conn, _) do
    conn
    |> put_status(:forbidden)
    |> put_view(StepFlow.WorkflowDefinitionView)
    |> render("error.json",
      errors: %{reason: "Forbidden to show workflow with this identifier"}
    )
  end

  def get(conn, %{"identifier" => workflow_identifier} = _params) do
    workflow =
      case workflow_identifier do
        _ -> %{}
      end

    conn
    |> json(workflow)
  end

  def get(conn, _params) do
    conn
    |> json(%{})
  end

  def statistics(
        %Plug.Conn{assigns: %{current_user: user}} = conn,
        params
      ) do
    start_date =
      params
      |> Map.get(
        "start_date",
        NaiveDateTime.utc_now()
        |> NaiveDateTime.add(-86_400, :second)
        |> NaiveDateTime.to_string()
      )
      |> NaiveDateTime.from_iso8601!()
      |> NaiveDateTime.truncate(:second)

    end_date =
      params
      |> Map.get(
        "end_date",
        NaiveDateTime.utc_now() |> NaiveDateTime.to_string()
      )
      |> NaiveDateTime.from_iso8601!()
      |> NaiveDateTime.truncate(:second)

    time_interval =
      params
      |> Map.get("time_interval", define_time_interval(start_date, end_date))
      |> String.to_integer()
      |> Kernel.max(1)

    identifiers = Map.get(params, "identifiers", [])

    workflows_status =
      Workflows.Status.list_workflows_status(start_date, end_date, identifiers, user.roles)

    conn
    |> put_view(StepFlow.WorkflowView)
    |> render("statistics.json", %{
      workflows_status: workflows_status,
      time_interval: time_interval,
      end_date: end_date
    })
  end

  defp define_time_interval(start_date, end_date) do
    with number_bins <- Configuration.get_var_value(StepFlow.Workflows, :number_bins, 60) do
      NaiveDateTime.diff(end_date, start_date, :second)
      |> Kernel.div(number_bins)
      |> Integer.to_string()
    end
  end

  def update(conn, _params) do
    conn
    |> put_status(:forbidden)
    |> put_view(StepFlow.WorkflowDefinitionView)
    |> render("error.json",
      errors: %{reason: "Forbidden to update workflow with this identifier"}
    )
  end

  def delete(%Plug.Conn{assigns: %{current_user: user}} = conn, %{"id" => id}) do
    workflow = Workflows.get_workflow!(id)

    if Helpers.has_right?("workflow::" <> workflow.identifier, user, "delete") do
      with {:ok, %Workflow{}} <- Workflows.delete_workflow(workflow) do
        send_resp(conn, :no_content, "")
      end
    else
      conn
      |> put_status(:forbidden)
      |> put_view(StepFlow.WorkflowDefinitionView)
      |> render("error.json",
        errors: %{reason: "Forbidden to update workflow with this identifier"}
      )
    end
  end

  def delete(conn, _) do
    conn
    |> put_status(:forbidden)
    |> put_view(StepFlow.WorkflowDefinitionView)
    |> render("error.json",
      errors: %{reason: "Forbidden to delete workflow with this identifier"}
    )
  end
end