From 3e813951ed72787d5f2292a2690879f57d8b640f Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Thu, 26 Dec 2024 22:46:36 +0000 Subject: [PATCH 01/18] Initial API documentation for new endpoint --- docs/content/docs/Technical/api.md | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index 3a1bacdaa..0d92c4eaa 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -190,4 +190,46 @@ requests.post( headers={"Authorization": f"Bearer {self.api_token}"}, json={"value": ["Civilian-military interaction", "Protest"], "message": "This is a comment."}, ) +``` + + +### Create an incident +`POST /api/v2/incidents/new` creates a new incident. It has two required parameters: +- `description`, the incident's description. `description` should be a string of at least 8 characters. +- `sensitive`, a string array of the incident's sensitivity. That should be either `["Not Sensitive"]`, or any combination of the values `["Graphic Violence", "Deceptive or Misleading", "Personal Information Visible"]`. + +It also has many optional parameters: +- Any attribute, both core and custom. See below for more information on accessing attributes' API identifiers. +- `status`, the incident's status. By default, the incident will be created as "To Do". If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. +- `urls`, which should contain a list of urls to be archived as distinct pieces of source material. For more granular control over source material metadata, we recommend using the field in conjunction with the source material creation endpoint and the source material metadata update endpoint. + +Note that it is not currently possible to set an incident's Restrictions, Assignees, or deleted status from this endpoint. + +Attributes' names in the Atlos interface are different from their API identifiers: +- Core attributes have string names (such as `description` and `status`). +- Custom attributes are identified by a long ID. + +You can find attributes' API identifiers in the **Access** pane of your project. + +```python +requests.post( + f"https://platform.atlos.org/api/v2/incidents_new", + headers={"Authorization": f"Bearer {self.api_token}"}, + json={"description": "Test incident created via the API", + "sensitive": ["Not Sensitive"] + } +) + +requests.post( + f"https://platform.atlos.org/api/v2/incidents_new", + headers={"Authorization": f"Bearer {self.api_token}"}, + json={"description": "Test incident created via the API", + "sensitive": ["Not Sensitive"], + "more_info": "This incident was created via the Atlos API", + "status": "In Progress", + "urls": ["https://docs.atlos.org"], + # This ID is the identifer for the project's multi-select 'Impact' attribute + "cf7a3ed7-2c26-428a-b56c-2cc3f98d7a2c": ["Residential"] + } +) ``` \ No newline at end of file From 2ee40090be166ae5fbeb3b7ee4bada3bcd44fcd7 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 03:30:37 +0000 Subject: [PATCH 02/18] Update documentation to show it's possible to update an incident's Restrictions --- docs/content/docs/Technical/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index 0d92c4eaa..9e072a650 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -203,7 +203,7 @@ It also has many optional parameters: - `status`, the incident's status. By default, the incident will be created as "To Do". If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. - `urls`, which should contain a list of urls to be archived as distinct pieces of source material. For more granular control over source material metadata, we recommend using the field in conjunction with the source material creation endpoint and the source material metadata update endpoint. -Note that it is not currently possible to set an incident's Restrictions, Assignees, or deleted status from this endpoint. +Note that it is not currently possible to set an incident's Assignees or deleted status from this endpoint. Attributes' names in the Atlos interface are different from their API identifiers: - Core attributes have string names (such as `description` and `status`). From afd40040dc6112bd24cbbc122ce3af409ebdb865 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 05:18:49 +0000 Subject: [PATCH 03/18] Initial work on incident creation API endpoint --- platform/lib/platform/material.ex | 53 ++++++++- platform/lib/platform/material/media.ex | 111 +++++++++++++----- platform/lib/platform/permissions.ex | 4 + platform/lib/platform/updates.ex | 13 ++ .../controllers/api/api_v2_controller.ex | 75 ++++++++++-- .../live/new_live/new_component.ex | 2 +- platform/lib/platform_web/router.ex | 1 + .../platform_web/controllers/api_v2_test.exs | 108 +++++++++++++++++ 8 files changed, 323 insertions(+), 44 deletions(-) diff --git a/platform/lib/platform/material.ex b/platform/lib/platform/material.ex index be3648c55..cb819428f 100644 --- a/platform/lib/platform/material.ex +++ b/platform/lib/platform/material.ex @@ -23,6 +23,7 @@ defmodule Platform.Material do alias Platform.Uploads alias Platform.Accounts alias Platform.Permissions + alias Platform.API.APIToken defp hydrate_media_query(query, opts \\ []) do query @@ -315,7 +316,9 @@ defmodule Platform.Material do |> Repo.insert() end - def create_media_audited(%User{} = user, attrs \\ %{}, opts \\ []) do + def create_media_audited(user_or_token, attrs \\ %{}, opts \\ []) + + def create_media_audited(%User{} = user, attrs, opts) do changeset = %Media{} |> Media.changeset(attrs, user) @@ -385,6 +388,50 @@ defmodule Platform.Material do end end + def create_media_audited(%APIToken{} = api_token, attrs, opts) do + changeset = + %Media{} + |> Media.changeset(attrs, api_token) + + cond do + !changeset.valid? -> + {:error, changeset} + + true -> + Repo.transaction(fn -> + changeset = change_media(%Media{}, attrs, api_token) + + {:ok, media} = + changeset + |> Repo.insert() + + if Keyword.get(opts, :post_updates, true) do + {:ok, _} = + Updates.change_from_media_creation(media, api_token) + |> Updates.create_update_from_changeset() + end + + # Upload media, if provided + for url <- Ecto.Changeset.get_field(changeset, :urls_parsed) do + {:ok, version} = + create_media_version_audited(media, api_token, %{ + upload_type: :direct, + status: :pending, + source_url: url, + media_id: media.id + }) + + archive_media_version(version) + end + + # Schedule metadata generation + schedule_media_auto_metadata_update(media) + + media + end) + end + end + def find_raw_slug(slug) do val = String.split(slug, "-", parts: 2) |> tl() |> Enum.join("-") @@ -561,8 +608,8 @@ defmodule Platform.Material do %Ecto.Changeset{data: %Media{}} """ - def change_media(%Media{} = media, attrs \\ %{}, user \\ nil) do - Media.changeset(media, attrs, user) + def change_media(%Media{} = media, attrs \\ %{}, user_or_token \\ nil) do + Media.changeset(media, attrs, user_or_token) end @doc """ diff --git a/platform/lib/platform/material/media.ex b/platform/lib/platform/material/media.ex index 550141bdf..a2e73d86e 100644 --- a/platform/lib/platform/material/media.ex +++ b/platform/lib/platform/material/media.ex @@ -6,6 +6,8 @@ defmodule Platform.Material.Media do alias Platform.Material.MediaSubscription alias Platform.Projects alias Platform.Permissions + alias Platform.Accounts.User + alias Platform.API.APIToken alias __MODULE__ @primary_key {:id, :binary_id, autogenerate: true} @@ -95,7 +97,19 @@ defmodule Platform.Material.Media do end @doc false - def changeset(media, attrs, user \\ nil) do + def changeset(media, attrs, user_or_token \\ nil) do + {user, api_token, actor} = + case user_or_token do + %User{} -> + {user_or_token, nil, user_or_token} + + %APIToken{} -> + {nil, user_or_token, user_or_token} + + _ -> + {nil, nil, nil} + end + media |> cast(attrs, [ :attr_description, @@ -105,24 +119,30 @@ defmodule Platform.Material.Media do :deleted, :project_id, :urls, - :location + :location, + :attr_more_info, + :attr_restrictions, + :attr_tags ]) |> validate_required([:project_id], message: "Please select a project") |> populate_geolocation() # These are special attributes, since we define it at creation time. Eventually, it'd be nice to unify this logic with the attribute-specific editing logic. |> Attribute.validate_attribute(Attribute.get_attribute(:description), media, user: user, + api_token: api_token, required: true ) |> Attribute.validate_attribute(Attribute.get_attribute(:sensitive), media, user: user, + api_token: api_token, required: true ) |> Attribute.validate_attribute(Attribute.get_attribute(:date), media, user: user, + api_token: api_token, required: false ) - |> validate_project(user, media) + |> validate_project(media, user: user, api_token: api_token) |> parse_and_validate_validate_json_array(:urls, :urls_parsed) |> validate_url_list(:urls_parsed) |> then(fn cs -> @@ -138,6 +158,7 @@ defmodule Platform.Material.Media do attrs, changeset: cs, user: user, + api_token: api_token, verify_change_exists: false ) end) @@ -151,13 +172,21 @@ defmodule Platform.Material.Media do # We manually insert the project ID because the media hasn't been inserted yet, # so we can't get it from the media itself. Still, we want to check the user's permissions. - if !is_nil(user) && - Permissions.can_edit_media?(user, %{media | project_id: project_id}, attr) do + if !is_nil(actor) && + (case actor do + %User{} -> + Permissions.can_edit_media?(actor, %{media | project_id: project_id}, attr) + %APIToken{} -> + Permissions.can_edit_media?(actor, %{media | project_id: project_id}) + end) do cs # TODO: This is a good refactoring opportunity with the logic above |> cast(attrs, [:attr_tags]) - |> Attribute.validate_attribute(Attribute.get_attribute(:tags), media, + |> Attribute.validate_attribute( + Attribute.get_attribute(:tags), + media, user: user, + api_token: api_token, required: false ) else @@ -209,43 +238,61 @@ defmodule Platform.Material.Media do changeset end - def validate_project(changeset, user \\ nil, media \\ nil) do + def validate_project(changeset, media \\ [], opts) do + user = Keyword.get(opts, :user) + api_token = Keyword.get(opts, :api_token) + project_id = Ecto.Changeset.get_change(changeset, :project_id, :no_change) original_project_id = changeset.data.project_id - case project_id do - :no_change -> - changeset + cond do + !is_nil(user) -> + case project_id do + :no_change -> + changeset - new_project_id -> - new_project = Projects.get_project(new_project_id) - original_project = Projects.get_project(original_project_id) + new_project_id -> + new_project = Projects.get_project(new_project_id) + original_project = Projects.get_project(original_project_id) - cond do - !is_nil(media) && !is_nil(user) && !Permissions.can_edit_media?(user, media) -> - changeset - |> add_error(:project_id, "You cannot edit this incidents's project.") + cond do + !is_nil(media) && !is_nil(user) && !Permissions.can_edit_media?(user, media) -> + changeset + |> add_error(:project_id, "You cannot edit this incidents's project.") - !is_nil(project_id) && is_nil(new_project) -> - changeset - |> add_error(:project_id, "Project does not exist") + !is_nil(project_id) && is_nil(new_project) -> + changeset + |> add_error(:project_id, "Project does not exist") - !is_nil(user) && !is_nil(new_project) && - !Permissions.can_add_media_to_project?(user, new_project) -> - changeset - |> add_error(:project_id, "You cannot add incidents to this project.") + !is_nil(user) && !is_nil(new_project) && + !Permissions.can_add_media_to_project?(user, new_project) -> + changeset + |> add_error(:project_id, "You cannot add incidents to this project.") - !is_nil(user) && !is_nil(original_project) -> - changeset - |> add_error(:project_id, "You cannot remove media from projects!") + !is_nil(user) && !is_nil(original_project) -> + changeset + |> add_error(:project_id, "You cannot remove media from projects!") - is_nil(new_project) -> - changeset - |> add_error(:project_id, "You must select a project.") + is_nil(new_project) -> + changeset + |> add_error(:project_id, "You must select a project.") - true -> - changeset + true -> + changeset + end end + + !is_nil(api_token) -> + # case project_id do + # :no_change -> + changeset + + # new_project_id -> + # changeset + # |> add_error(:project_id, "You cannot edit incidents' projects via the API.") + # end + true -> + changeset end end diff --git a/platform/lib/platform/permissions.ex b/platform/lib/platform/permissions.ex index efe867eaf..e53dac273 100644 --- a/platform/lib/platform/permissions.ex +++ b/platform/lib/platform/permissions.ex @@ -120,6 +120,10 @@ defmodule Platform.Permissions do token.project_id == media.project_id and _is_media_editable?(media) end + def can_api_token_create_media?(%APIToken{} = token) do + Enum.member?(token.permissions, :edit) and token.is_active + end + def can_api_token_edit_media?(%APIToken{} = token, %Media{} = media) do Enum.member?(token.permissions, :edit) and token.is_active and token.project_id == media.project_id and _is_media_editable?(media) diff --git a/platform/lib/platform/updates.ex b/platform/lib/platform/updates.ex index 1eff115b9..ab7cb216b 100644 --- a/platform/lib/platform/updates.ex +++ b/platform/lib/platform/updates.ex @@ -308,6 +308,8 @@ defmodule Platform.Updates do @doc """ Helper API function that takes attribute change information and uses it to create an Update changeset. Requires 'explanation' to be in attrs. """ + def change_from_media_creation(media, user_or_token) + def change_from_media_creation(%Media{} = media, %User{} = user) do change_update( %Update{}, @@ -319,6 +321,17 @@ defmodule Platform.Updates do ) end + def change_from_media_creation(%Media{} = media, %APIToken{} = api_token) do + change_update( + %Update{}, + media, + api_token, + %{ + "type" => :create + } + ) + end + @doc """ Helper API function that takes attribute change information and uses it to create an Update changeset. """ diff --git a/platform/lib/platform_web/controllers/api/api_v2_controller.ex b/platform/lib/platform_web/controllers/api/api_v2_controller.ex index fd8722e4d..40d53c19d 100644 --- a/platform/lib/platform_web/controllers/api/api_v2_controller.ex +++ b/platform/lib/platform_web/controllers/api/api_v2_controller.ex @@ -92,6 +92,61 @@ defmodule PlatformWeb.APIV2Controller do end end + def create_media(conn, params) do + project_id = conn.assigns.token.project_id + project = Projects.get_project!(project_id) + + cond do + not Permissions.can_api_token_create_media?(conn.assigns.token) -> + json(conn |> put_status(401), %{error: "unauthorized"}) + + true -> + # Generate attribute parameters for all attributes in the input + media_params = + params + |> Enum.reduce(%{}, fn {key, value}, acc -> + case Attribute.get_attribute(key, project: project) do + nil -> + acc + + attr -> + # Merge the generated attribute change params + Map.merge( + acc, + Material.generate_attribute_change_params( + attr, + value, + project + ) + ) + end + end) + |> Map.put("project_id", project_id) + + # We expect a JSON array of URLs in the incident creation flow + if not is_nil(params["urls"]) do + media_params |> Map.put("urls", Jason.encode!(params["urls"])) + end + + case Material.create_media_audited(conn.assigns.token, media_params) do + {:ok, media} -> + media_with_project = + Platform.Repo.preload(media, [:project, :versions]) + + Auditor.log( + :media_created, + Map.merge(media_params, %{media_slug: media_with_project.slug}), + conn + ) + + json(conn, %{success: true, result: media_with_project}) + + {:error, changeset} -> + json(conn |> put_status(401), %{error: render_changeset_errors(changeset)}) + end + end + end + def create_media_version(conn, params) do media_id = params["slug"] url = params["url"] @@ -350,20 +405,24 @@ defmodule PlatformWeb.APIV2Controller do end end + # TODO this def isn't the best way to format elixir: defp render_changeset_errors(changeset) do - Enum.map(changeset.errors, fn {field, detail} -> - {field, render_detail(detail)} + Enum.map(changeset.errors, fn + {field, {"is invalid", [type: {:array, type}, validation: :cast]}} -> + {field, "must be an array of #{type}s"} + + {field, {message, values}} -> + {field, render_detail(message, values)} + + {field, message} -> + {field, message} end) - |> Enum.into(%{}) + |> Map.new() end - defp render_detail({message, values}) do + defp render_detail(message, values) do Enum.reduce(values, message, fn {k, v}, acc -> String.replace(acc, "%{#{k}}", to_string(v)) end) end - - defp render_detail(message) do - message - end end diff --git a/platform/lib/platform_web/live/new_live/new_component.ex b/platform/lib/platform_web/live/new_live/new_component.ex index 6c2016324..0dec94d87 100644 --- a/platform/lib/platform_web/live/new_live/new_component.ex +++ b/platform/lib/platform_web/live/new_live/new_component.ex @@ -113,7 +113,7 @@ defmodule PlatformWeb.NewLive.NewComponent do Material.change_media( socket.assigns.media, media_params, - socket.assigns.current_user + user: socket.assigns.current_user ) ) |> push_redirect(to: "/incidents/#{media.slug}")} diff --git a/platform/lib/platform_web/router.ex b/platform/lib/platform_web/router.ex index b14455c0b..dc74585e0 100644 --- a/platform/lib/platform_web/router.ex +++ b/platform/lib/platform_web/router.ex @@ -91,6 +91,7 @@ defmodule PlatformWeb.Router do get("/source_material", APIV2Controller, :media_versions) get("/source_material/:id", APIV2Controller, :media_version) post("/source_material/new/:slug", APIV2Controller, :create_media_version) + post("/incidents/new", APIV2Controller, :create_media) post( "/source_material/metadata/:id/:namespace", diff --git a/platform/test/platform_web/controllers/api_v2_test.exs b/platform/test/platform_web/controllers/api_v2_test.exs index 2d094fb1a..667a0d324 100644 --- a/platform/test/platform_web/controllers/api_v2_test.exs +++ b/platform/test/platform_web/controllers/api_v2_test.exs @@ -555,4 +555,112 @@ defmodule PlatformWeb.APIV2Test do %{"success" => true, "result" => ^version} = json_response(conn, 200) end + + test "POST /incidents/new" do + project = project_fixture() + + underpermissioned_token = + api_token_fixture(%{project_id: project.id, permissions: [:read, :comment]}) + + token = api_token_fixture(%{project_id: project.id, permissions: [:read, :comment, :edit]}) + + # Create a piece of media + auth_conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{ + "sensitive" => ["Not Sensitive"], + "description" => "Test incident description" + }) + + %{"success" => true, "result" => media} = json_response(auth_conn, 200) + assert media["attr_sensitive"] == ["Not Sensitive"] + assert media["attr_description"] == "Test incident description" + + # Quickly check that permission validation is working + noauth_conn = post(build_conn(), "/api/v2/incidents/new", %{}) + assert json_response(noauth_conn, 401) == %{"error" => "invalid token or token not found"} + + # Check that underpermissioned tokens can't create incidents + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> underpermissioned_token.value) + |> post("/api/v2/incidents/new", %{}) + + assert json_response(conn, 401) == %{"error" => "unauthorized"} + + # Check that validation for required attributes is working pt. 1 + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{ + "sensitive" => ["Not Sensitive"] + }) + + assert json_response(conn, 401) == %{"error" => %{"attr_description" => "can't be blank"}} + + # Check that validation for required attributes is working pt. 2 + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{ + "description" => "Test incident name" + }) + + assert json_response(conn, 401) == %{"error" => %{"attr_sensitive" => "can't be blank"}} + + # Check that validation for required attributes is working pt. 3 + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{}) + + assert json_response(conn, 401) == %{ + "error" => %{ + "attr_description" => "can't be blank", + "attr_sensitive" => "can't be blank" + } + } + + # Ensure length validation of required attributes is passed in API response + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{ + "description" => "Short", + "sensitive" => ["Not Sensitive"] + }) + + assert json_response(conn, 401) == %{ + "error" => %{"attr_description" => "should be at least 8 character(s)"} + } + + # Ensure type validation of required attributes is passed in API response + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{ + "description" => "Test incident description", + "sensitive" => "Not Sensitive" + }) + + assert json_response(conn, 401) == %{ + "error" => %{"attr_sensitive" => "must be an array of strings"} + } + + # Ensure type validation of optional attributes is passed in API response + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token.value) + |> post("/api/v2/incidents/new", %{ + "description" => "Test incident description", + "sensitive" => ["Not Sensitive"], + "more_info" => ["This More Info section is invalid because", "It's in an array"] + }) + + assert json_response(conn, 401) == %{"error" => %{"attr_more_info" => "is invalid"}} + + # TODO check URL validation fails loudly + # TODO ensure arbitrary optional attribute values are stored correctly + end end From b1ed15b889a6080591e261ecbee40c4110eee57a Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 05:42:10 +0000 Subject: [PATCH 04/18] Remove extraneous check for value of API token --- platform/lib/platform/material/media.ex | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/platform/lib/platform/material/media.ex b/platform/lib/platform/material/media.ex index a2e73d86e..b35ce94fc 100644 --- a/platform/lib/platform/material/media.ex +++ b/platform/lib/platform/material/media.ex @@ -142,7 +142,7 @@ defmodule Platform.Material.Media do api_token: api_token, required: false ) - |> validate_project(media, user: user, api_token: api_token) + |> validate_project(media, user: user) |> parse_and_validate_validate_json_array(:urls, :urls_parsed) |> validate_url_list(:urls_parsed) |> then(fn cs -> @@ -240,7 +240,6 @@ defmodule Platform.Material.Media do def validate_project(changeset, media \\ [], opts) do user = Keyword.get(opts, :user) - api_token = Keyword.get(opts, :api_token) project_id = Ecto.Changeset.get_change(changeset, :project_id, :no_change) original_project_id = changeset.data.project_id @@ -281,16 +280,6 @@ defmodule Platform.Material.Media do changeset end end - - !is_nil(api_token) -> - # case project_id do - # :no_change -> - changeset - - # new_project_id -> - # changeset - # |> add_error(:project_id, "You cannot edit incidents' projects via the API.") - # end true -> changeset end From 2abc7a0db4e6450a732f68ea26eda3bcd7ed8145 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 06:00:02 +0000 Subject: [PATCH 05/18] Improve API docs --- docs/content/docs/Technical/api.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index 9e072a650..212776706 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -201,7 +201,9 @@ requests.post( It also has many optional parameters: - Any attribute, both core and custom. See below for more information on accessing attributes' API identifiers. - `status`, the incident's status. By default, the incident will be created as "To Do". If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. -- `urls`, which should contain a list of urls to be archived as distinct pieces of source material. For more granular control over source material metadata, we recommend using the field in conjunction with the source material creation endpoint and the source material metadata update endpoint. +- `restrictions`, an array of the incident's restrictions. If you include this field, you can set the incident to one or both of `["Hidden", "Frozen"]`. +- `tags`, an array of the incident's tags. If you include a value not yet present in the project, that tag will be created and applied to the incident. +- `urls`, which should contain a list of urls to be archived as distinct pieces of source material. Unlike the other fields listed here, validation of URLs fails silently: if an invalid URL is included in this array, Atlos will create the incident without the relevant piece of source material. For more granular control over source material metadata, we recommend using the [source material creation endpoint](#create-a-new-piece-of-source-material) and the [source material metadata update endpoint](#set-source-material-metadata). Note that it is not currently possible to set an incident's Assignees or deleted status from this endpoint. From ecba29c04352735769a482bd073f9bb4e5414c7c Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 06:06:41 +0000 Subject: [PATCH 06/18] Improve documentation of API token permissions --- .../lib/platform_web/live/projects_live/api_tokens_component.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/lib/platform_web/live/projects_live/api_tokens_component.ex b/platform/lib/platform_web/live/projects_live/api_tokens_component.ex index 1ca8752f6..a617e0e36 100644 --- a/platform/lib/platform_web/live/projects_live/api_tokens_component.ex +++ b/platform/lib/platform_web/live/projects_live/api_tokens_component.ex @@ -390,7 +390,7 @@ defmodule PlatformWeb.ProjectsLive.APITokensComponent do "read" => "Can read incidents and comments, including hidden and restricted incidents", "comment" => "Can add comments to incidents", - "edit" => "Can edit incidents' attributes and metadata" + "edit" => "Can create incidents and edit incidents' attributes and metadata" }), "data-required": Jason.encode!(["read"]) ) %> From ed73522beb50668015ccb13741348da3ae14152b Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 06:07:47 +0000 Subject: [PATCH 07/18] Clean up tests --- platform/test/platform_web/controllers/api_v2_test.exs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/platform/test/platform_web/controllers/api_v2_test.exs b/platform/test/platform_web/controllers/api_v2_test.exs index 667a0d324..a5e7b447f 100644 --- a/platform/test/platform_web/controllers/api_v2_test.exs +++ b/platform/test/platform_web/controllers/api_v2_test.exs @@ -570,12 +570,14 @@ defmodule PlatformWeb.APIV2Test do |> put_req_header("authorization", "Bearer " <> token.value) |> post("/api/v2/incidents/new", %{ "sensitive" => ["Not Sensitive"], - "description" => "Test incident description" + "description" => "Test incident description", + "more_info" => "More info about this incident" }) %{"success" => true, "result" => media} = json_response(auth_conn, 200) assert media["attr_sensitive"] == ["Not Sensitive"] assert media["attr_description"] == "Test incident description" + assert media["attr_more_info"] == "More info about this incident" # Quickly check that permission validation is working noauth_conn = post(build_conn(), "/api/v2/incidents/new", %{}) @@ -660,7 +662,6 @@ defmodule PlatformWeb.APIV2Test do assert json_response(conn, 401) == %{"error" => %{"attr_more_info" => "is invalid"}} - # TODO check URL validation fails loudly - # TODO ensure arbitrary optional attribute values are stored correctly + # Ensure arbitrary optional attribute values are stored correctly end end From 906bb46954998bb94ddf03494934c8bd5d54fd50 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:38:07 +0000 Subject: [PATCH 08/18] Cleanup type checking --- platform/lib/platform/updates.ex | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/platform/lib/platform/updates.ex b/platform/lib/platform/updates.ex index ab7cb216b..86ba747b6 100644 --- a/platform/lib/platform/updates.ex +++ b/platform/lib/platform/updates.ex @@ -308,27 +308,12 @@ defmodule Platform.Updates do @doc """ Helper API function that takes attribute change information and uses it to create an Update changeset. Requires 'explanation' to be in attrs. """ - def change_from_media_creation(media, user_or_token) - - def change_from_media_creation(%Media{} = media, %User{} = user) do + def change_from_media_creation(%Media{} = media, user_or_token) when is_struct(user_or_token, User) or is_struct(user_or_token, APIToken) do change_update( %Update{}, media, - user, - %{ - "type" => :create - } - ) - end - - def change_from_media_creation(%Media{} = media, %APIToken{} = api_token) do - change_update( - %Update{}, - media, - api_token, - %{ - "type" => :create - } + user_or_token, + %{"type" => :create} ) end From 05c51571715b585e0d1bdf06c5238abf77a5070e Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Sat, 28 Dec 2024 01:29:26 -0800 Subject: [PATCH 09/18] Correct endpoint URL in documentation --- docs/content/docs/Technical/api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index 212776706..c8765ee68 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -215,7 +215,7 @@ You can find attributes' API identifiers in the **Access** pane of your project. ```python requests.post( - f"https://platform.atlos.org/api/v2/incidents_new", + f"https://platform.atlos.org/api/v2/incidents/new", headers={"Authorization": f"Bearer {self.api_token}"}, json={"description": "Test incident created via the API", "sensitive": ["Not Sensitive"] @@ -223,7 +223,7 @@ requests.post( ) requests.post( - f"https://platform.atlos.org/api/v2/incidents_new", + f"https://platform.atlos.org/api/v2/incidents/new", headers={"Authorization": f"Bearer {self.api_token}"}, json={"description": "Test incident created via the API", "sensitive": ["Not Sensitive"], @@ -234,4 +234,4 @@ requests.post( "cf7a3ed7-2c26-428a-b56c-2cc3f98d7a2c": ["Residential"] } ) -``` \ No newline at end of file +``` From 5a2403ab303b416484e76348e80fc75f708a8632 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Sat, 28 Dec 2024 19:19:17 -0800 Subject: [PATCH 10/18] Format default status as code snippet Co-authored-by: R. Miles McCain --- docs/content/docs/Technical/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index c8765ee68..b16a8ddc5 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -200,7 +200,7 @@ requests.post( It also has many optional parameters: - Any attribute, both core and custom. See below for more information on accessing attributes' API identifiers. -- `status`, the incident's status. By default, the incident will be created as "To Do". If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. +- `status`, the incident's status. By default, the incident will be created as `"To Do"`. If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. - `restrictions`, an array of the incident's restrictions. If you include this field, you can set the incident to one or both of `["Hidden", "Frozen"]`. - `tags`, an array of the incident's tags. If you include a value not yet present in the project, that tag will be created and applied to the incident. - `urls`, which should contain a list of urls to be archived as distinct pieces of source material. Unlike the other fields listed here, validation of URLs fails silently: if an invalid URL is included in this array, Atlos will create the incident without the relevant piece of source material. For more granular control over source material metadata, we recommend using the [source material creation endpoint](#create-a-new-piece-of-source-material) and the [source material metadata update endpoint](#set-source-material-metadata). From df7a9aa0d8d29111fd61cf76faaa7b8f297226b3 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Sat, 28 Dec 2024 19:19:58 -0800 Subject: [PATCH 11/18] Simplify type check / pattern matching Co-authored-by: R. Miles McCain --- platform/lib/platform/updates.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/lib/platform/updates.ex b/platform/lib/platform/updates.ex index 86ba747b6..a220855ff 100644 --- a/platform/lib/platform/updates.ex +++ b/platform/lib/platform/updates.ex @@ -308,7 +308,7 @@ defmodule Platform.Updates do @doc """ Helper API function that takes attribute change information and uses it to create an Update changeset. Requires 'explanation' to be in attrs. """ - def change_from_media_creation(%Media{} = media, user_or_token) when is_struct(user_or_token, User) or is_struct(user_or_token, APIToken) do + def change_from_media_creation(%Media{} = media, %_{} = user_or_token) when user_or_token in [%User{}, %APIToken{}] do change_update( %Update{}, media, From 75a84a3e68e664a8e5f2584018a65acd58267004 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:22:03 -0800 Subject: [PATCH 12/18] Revert type checking simplification Pattern matching failed under the simplified version. --- platform/lib/platform/updates.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/lib/platform/updates.ex b/platform/lib/platform/updates.ex index a220855ff..48070736a 100644 --- a/platform/lib/platform/updates.ex +++ b/platform/lib/platform/updates.ex @@ -308,7 +308,7 @@ defmodule Platform.Updates do @doc """ Helper API function that takes attribute change information and uses it to create an Update changeset. Requires 'explanation' to be in attrs. """ - def change_from_media_creation(%Media{} = media, %_{} = user_or_token) when user_or_token in [%User{}, %APIToken{}] do + def change_from_media_creation(%Media{} = media, %_{} = user_or_token) when is_struct(user_or_token, User) or is_struct(user_or_token, APIToken) do change_update( %Update{}, media, From 47c6d4999a336570b88937edb85f3063b1df58be Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Mon, 30 Dec 2024 22:34:08 +0000 Subject: [PATCH 13/18] Improve documentation for project validation --- platform/lib/platform/material/media.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platform/lib/platform/material/media.ex b/platform/lib/platform/material/media.ex index b35ce94fc..1ad9f4577 100644 --- a/platform/lib/platform/material/media.ex +++ b/platform/lib/platform/material/media.ex @@ -238,6 +238,9 @@ defmodule Platform.Material.Media do changeset end + @doc """ + Validates changes to a piece of media's project. + """ def validate_project(changeset, media \\ [], opts) do user = Keyword.get(opts, :user) From 08d25c7053e22344f2f5d6318429f11c86be0ae7 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:38:08 +0000 Subject: [PATCH 14/18] Aggregate media creation logic --- platform/lib/platform/material.ex | 104 +++++++++--------------------- 1 file changed, 30 insertions(+), 74 deletions(-) diff --git a/platform/lib/platform/material.ex b/platform/lib/platform/material.ex index cb819428f..ed9474bde 100644 --- a/platform/lib/platform/material.ex +++ b/platform/lib/platform/material.ex @@ -316,12 +316,10 @@ defmodule Platform.Material do |> Repo.insert() end - def create_media_audited(user_or_token, attrs \\ %{}, opts \\ []) - - def create_media_audited(%User{} = user, attrs, opts) do + def create_media_audited(user_or_token, attrs \\ %{}, opts \\ []) when is_struct(user_or_token, User) or is_struct(user_or_token, APIToken) do changeset = %Media{} - |> Media.changeset(attrs, user) + |> Media.changeset(attrs, user_or_token) cond do !changeset.valid? -> @@ -329,7 +327,7 @@ defmodule Platform.Material do true -> Repo.transaction(fn -> - changeset = change_media(%Media{}, attrs, user) + changeset = change_media(%Media{}, attrs, user_or_token) {:ok, media} = changeset @@ -337,84 +335,42 @@ defmodule Platform.Material do if Keyword.get(opts, :post_updates, true) do {:ok, _} = - Updates.change_from_media_creation(media, user) + Updates.change_from_media_creation(media, user_or_token) |> Updates.create_update_from_changeset() end - # Automatically tag new incidents created by regular users, if desirable - user_project_membership = - Projects.get_project_membership_by_user_and_project_id(user, media.project_id) - - {:ok, media} = - with false <- Enum.member?([:owner, :manager], user_project_membership.role), - new_tags_json <- System.get_env("AUTOTAG_USER_INCIDENTS"), - false <- is_nil(new_tags_json) or String.trim(new_tags_json) == "", - {:ok, new_tags} <- Jason.decode(new_tags_json), - false <- Enum.empty?(new_tags) do - {:ok, new_media} = - update_media_attribute_audited( - media, - Attribute.get_attribute(:tags), - Accounts.get_auto_account(), - %{"attr_tags" => (media.attr_tags || []) ++ new_tags} - ) + if is_struct(user_or_token, User) do + # Automatically tag new incidents created by regular users, if desirable + user_project_membership = + Projects.get_project_membership_by_user_and_project_id(user_or_token, media.project_id) + + {:ok, media} = + with false <- Enum.member?([:owner, :manager], user_project_membership.role), + new_tags_json <- System.get_env("AUTOTAG_USER_INCIDENTS"), + false <- is_nil(new_tags_json) or String.trim(new_tags_json) == "", + {:ok, new_tags} <- Jason.decode(new_tags_json), + false <- Enum.empty?(new_tags) do + {:ok, new_media} = + update_media_attribute_audited( + media, + Attribute.get_attribute(:tags), + Accounts.get_auto_account(), + %{"attr_tags" => (media.attr_tags || []) ++ new_tags} + ) + + {:ok, new_media} + else + _ -> {:ok, media} + end - {:ok, new_media} - else - _ -> {:ok, media} + # Subscribe the creator + {:ok, _} = subscribe_user(media, user_or_token) end - # Subscribe the creator - {:ok, _} = subscribe_user(media, user) - - # Upload media, if provided - for url <- Ecto.Changeset.get_field(changeset, :urls_parsed) do - {:ok, version} = - create_media_version_audited(media, user, %{ - upload_type: :direct, - status: :pending, - source_url: url, - media_id: media.id - }) - - archive_media_version(version) - end - - # Schedule metadata generation - schedule_media_auto_metadata_update(media) - - media - end) - end - end - - def create_media_audited(%APIToken{} = api_token, attrs, opts) do - changeset = - %Media{} - |> Media.changeset(attrs, api_token) - - cond do - !changeset.valid? -> - {:error, changeset} - - true -> - Repo.transaction(fn -> - changeset = change_media(%Media{}, attrs, api_token) - - {:ok, media} = - changeset - |> Repo.insert() - - if Keyword.get(opts, :post_updates, true) do - {:ok, _} = - Updates.change_from_media_creation(media, api_token) - |> Updates.create_update_from_changeset() - end - # Upload media, if provided for url <- Ecto.Changeset.get_field(changeset, :urls_parsed) do {:ok, version} = - create_media_version_audited(media, api_token, %{ + create_media_version_audited(media, user_or_token, %{ upload_type: :direct, status: :pending, source_url: url, From 121b0c2a45fa2f23cf9c45afc8d1de9405c17e49 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:45:46 +0000 Subject: [PATCH 15/18] Improve project validation --- platform/lib/platform/material/media.ex | 20 +++++++++---------- platform/lib/platform/permissions.ex | 9 ++++++++- .../controllers/api/api_v2_controller.ex | 1 - 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/platform/lib/platform/material/media.ex b/platform/lib/platform/material/media.ex index 1ad9f4577..5fb3e1e60 100644 --- a/platform/lib/platform/material/media.ex +++ b/platform/lib/platform/material/media.ex @@ -142,7 +142,7 @@ defmodule Platform.Material.Media do api_token: api_token, required: false ) - |> validate_project(media, user: user) + |> validate_project(media, user_or_token) |> parse_and_validate_validate_json_array(:urls, :urls_parsed) |> validate_url_list(:urls_parsed) |> then(fn cs -> @@ -241,14 +241,12 @@ defmodule Platform.Material.Media do @doc """ Validates changes to a piece of media's project. """ - def validate_project(changeset, media \\ [], opts) do - user = Keyword.get(opts, :user) - + def validate_project(changeset, media \\ [], user_or_token \\ :nil) do project_id = Ecto.Changeset.get_change(changeset, :project_id, :no_change) original_project_id = changeset.data.project_id cond do - !is_nil(user) -> + !is_nil(user_or_token) -> case project_id do :no_change -> changeset @@ -258,7 +256,7 @@ defmodule Platform.Material.Media do original_project = Projects.get_project(original_project_id) cond do - !is_nil(media) && !is_nil(user) && !Permissions.can_edit_media?(user, media) -> + !is_nil(media) && !is_nil(user_or_token) && !Permissions.can_edit_media?(user_or_token, media) -> changeset |> add_error(:project_id, "You cannot edit this incidents's project.") @@ -266,12 +264,12 @@ defmodule Platform.Material.Media do changeset |> add_error(:project_id, "Project does not exist") - !is_nil(user) && !is_nil(new_project) && - !Permissions.can_add_media_to_project?(user, new_project) -> + !is_nil(user_or_token) && !is_nil(new_project) && + !Permissions.can_add_media_to_project?(user_or_token, new_project) -> changeset |> add_error(:project_id, "You cannot add incidents to this project.") - !is_nil(user) && !is_nil(original_project) -> + !is_nil(user_or_token) && !is_nil(original_project) -> changeset |> add_error(:project_id, "You cannot remove media from projects!") @@ -283,7 +281,7 @@ defmodule Platform.Material.Media do changeset end end - true -> + true -> changeset end end @@ -315,7 +313,7 @@ defmodule Platform.Material.Media do def project_changeset(media, attrs, user \\ nil) do media |> cast(attrs, [:project_id]) - |> validate_project(user, media) + |> validate_project(media, user) end @doc """ diff --git a/platform/lib/platform/permissions.ex b/platform/lib/platform/permissions.ex index e53dac273..b67b77a76 100644 --- a/platform/lib/platform/permissions.ex +++ b/platform/lib/platform/permissions.ex @@ -126,7 +126,10 @@ defmodule Platform.Permissions do def can_api_token_edit_media?(%APIToken{} = token, %Media{} = media) do Enum.member?(token.permissions, :edit) and token.is_active and - token.project_id == media.project_id and _is_media_editable?(media) + _is_media_editable?(media) and + + # If media is being created and doesn't yet have a project, the token should able to give the media a project + (token.project_id == media.project_id or is_nil(media.project_id)) end def can_api_token_update_attribute?( @@ -157,6 +160,10 @@ defmodule Platform.Permissions do end end + def can_add_media_to_project?(%APIToken{} = token, %Project{} = project) do + Enum.member?(token.permissions, :edit) and token.is_active and project.active + end + def can_bulk_upload_media_to_project?(%User{}, %Project{active: false}) do false end diff --git a/platform/lib/platform_web/controllers/api/api_v2_controller.ex b/platform/lib/platform_web/controllers/api/api_v2_controller.ex index 40d53c19d..6cd610e41 100644 --- a/platform/lib/platform_web/controllers/api/api_v2_controller.ex +++ b/platform/lib/platform_web/controllers/api/api_v2_controller.ex @@ -405,7 +405,6 @@ defmodule PlatformWeb.APIV2Controller do end end - # TODO this def isn't the best way to format elixir: defp render_changeset_errors(changeset) do Enum.map(changeset.errors, fn {field, {"is invalid", [type: {:array, type}, validation: :cast]}} -> From 0c349da0d295a739a1e99a1d268923793d3ef2c2 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:40:16 +0000 Subject: [PATCH 16/18] Fix bug that prevented URL validation in API --- docs/content/docs/Technical/api.md | 2 +- .../lib/platform_web/controllers/api/api_v2_controller.ex | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index b16a8ddc5..34ad2dacc 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -203,7 +203,7 @@ It also has many optional parameters: - `status`, the incident's status. By default, the incident will be created as `"To Do"`. If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. - `restrictions`, an array of the incident's restrictions. If you include this field, you can set the incident to one or both of `["Hidden", "Frozen"]`. - `tags`, an array of the incident's tags. If you include a value not yet present in the project, that tag will be created and applied to the incident. -- `urls`, which should contain a list of urls to be archived as distinct pieces of source material. Unlike the other fields listed here, validation of URLs fails silently: if an invalid URL is included in this array, Atlos will create the incident without the relevant piece of source material. For more granular control over source material metadata, we recommend using the [source material creation endpoint](#create-a-new-piece-of-source-material) and the [source material metadata update endpoint](#set-source-material-metadata). +- `urls`, which should contain a list of URLs to be archived as distinct pieces of source material. URLs must begin with "https://" or "http://". For more granular control over source material metadata, we recommend using the [source material creation endpoint](#create-a-new-piece-of-source-material) and the [source material metadata update endpoint](#set-source-material-metadata). Note that it is not currently possible to set an incident's Assignees or deleted status from this endpoint. diff --git a/platform/lib/platform_web/controllers/api/api_v2_controller.ex b/platform/lib/platform_web/controllers/api/api_v2_controller.ex index 6cd610e41..16dd696ad 100644 --- a/platform/lib/platform_web/controllers/api/api_v2_controller.ex +++ b/platform/lib/platform_web/controllers/api/api_v2_controller.ex @@ -124,8 +124,9 @@ defmodule PlatformWeb.APIV2Controller do |> Map.put("project_id", project_id) # We expect a JSON array of URLs in the incident creation flow - if not is_nil(params["urls"]) do - media_params |> Map.put("urls", Jason.encode!(params["urls"])) + media_params = case params["urls"] do + nil -> media_params + urls -> Map.put(media_params, "urls", Jason.encode!(urls)) end case Material.create_media_audited(conn.assigns.token, media_params) do From 1e7a9c33e0b5a1a15e7ffc90d8925ff3546d0837 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:00:09 +0000 Subject: [PATCH 17/18] Update assignments documentation --- docs/content/docs/Technical/api.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index 34ad2dacc..b97037415 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -203,15 +203,16 @@ It also has many optional parameters: - `status`, the incident's status. By default, the incident will be created as `"To Do"`. If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. - `restrictions`, an array of the incident's restrictions. If you include this field, you can set the incident to one or both of `["Hidden", "Frozen"]`. - `tags`, an array of the incident's tags. If you include a value not yet present in the project, that tag will be created and applied to the incident. +- `assignments`, an array of the users assigned to the incident. Use users' IDs (not their usernames) in the API. - `urls`, which should contain a list of URLs to be archived as distinct pieces of source material. URLs must begin with "https://" or "http://". For more granular control over source material metadata, we recommend using the [source material creation endpoint](#create-a-new-piece-of-source-material) and the [source material metadata update endpoint](#set-source-material-metadata). -Note that it is not currently possible to set an incident's Assignees or deleted status from this endpoint. +Note that it is not currently possible to set an incident's deleted status from this endpoint. -Attributes' names in the Atlos interface are different from their API identifiers: +Attributes' and users' names in the Atlos interface are different from their API identifiers: - Core attributes have string names (such as `description` and `status`). -- Custom attributes are identified by a long ID. +- Custom attributes and users are identified by a long ID. -You can find attributes' API identifiers in the **Access** pane of your project. +You can find attributes' and users' API identifiers in the **Access** pane of your project. ```python requests.post( From 2ad6551a021baff9f9aeb6d7cc712179faf9ad78 Mon Sep 17 00:00:00 2001 From: Noah Schechter <100018299+noah-schechter@users.noreply.github.com> Date: Thu, 2 Jan 2025 21:23:20 +0000 Subject: [PATCH 18/18] Revert assignments for now --- docs/content/docs/Technical/api.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/content/docs/Technical/api.md b/docs/content/docs/Technical/api.md index b97037415..6e7a2382a 100644 --- a/docs/content/docs/Technical/api.md +++ b/docs/content/docs/Technical/api.md @@ -203,16 +203,15 @@ It also has many optional parameters: - `status`, the incident's status. By default, the incident will be created as `"To Do"`. If you include this field, you can set the incident to one of: `"To Do"`, `"In Progress"`, `"Ready for Review"`, `"Help Needed"`, `"Completed"`, or "`Canceled"`. - `restrictions`, an array of the incident's restrictions. If you include this field, you can set the incident to one or both of `["Hidden", "Frozen"]`. - `tags`, an array of the incident's tags. If you include a value not yet present in the project, that tag will be created and applied to the incident. -- `assignments`, an array of the users assigned to the incident. Use users' IDs (not their usernames) in the API. - `urls`, which should contain a list of URLs to be archived as distinct pieces of source material. URLs must begin with "https://" or "http://". For more granular control over source material metadata, we recommend using the [source material creation endpoint](#create-a-new-piece-of-source-material) and the [source material metadata update endpoint](#set-source-material-metadata). -Note that it is not currently possible to set an incident's deleted status from this endpoint. +Note that it is not currently possible to set an incident's deleted status or its Assignments from this endpoint. -Attributes' and users' names in the Atlos interface are different from their API identifiers: +Attributes' names in the Atlos interface are different from their API identifiers: - Core attributes have string names (such as `description` and `status`). -- Custom attributes and users are identified by a long ID. +- Custom attributes are identified by a long ID. -You can find attributes' and users' API identifiers in the **Access** pane of your project. +You can find attributes' API identifiers in the **Access** pane of your project. ```python requests.post(