Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy recent changes to production #1008

Merged
merged 22 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
057d832
Initial attribute grouping work
milesmcc Jun 23, 2024
2453809
Fix modals in attribute editor
milesmcc Jun 29, 2024
269a844
Fix metadata being overwritten
milesmcc Jun 29, 2024
6906b45
Make groups and attributes orderable
milesmcc Jun 29, 2024
8094b03
Allow editing and deleting groups
milesmcc Jun 29, 2024
111cc14
Repositioning design improvements and bug fixes
milesmcc Jun 30, 2024
9030106
Display color
milesmcc Jun 30, 2024
a575236
Add group info to incident page
milesmcc Jun 30, 2024
5149631
Include groups in new incident creation form
milesmcc Jun 30, 2024
0e7ffb2
Add groupings to table view
milesmcc Jun 30, 2024
e015d63
Clarify copy on showing groups in creation modal
noah-schechter Jun 30, 2024
7d234fa
Make page/window language consistent
noah-schechter Jun 30, 2024
a8005d9
Don't allow empty updates (closes #1014)
milesmcc Jul 1, 2024
2e28602
Always close group editing modal on save (closes #1013)
milesmcc Jul 1, 2024
e7c8019
Fix group editing buttons in Safari (closes #1012)
milesmcc Jul 1, 2024
fa7629f
Merge pull request #1010 from atlosdotorg/noah-schechter-patch-1
milesmcc Jul 1, 2024
7c10fab
Add metadata viewer to source material
milesmcc Jul 1, 2024
a1316b0
Fix group editing after reordering; format
milesmcc Jul 1, 2024
c2891aa
Don't lock in attribute types when creating an attribute (closes #1009)
milesmcc Jul 1, 2024
40aa1df
Hide overflow in source material metadata
milesmcc Jul 1, 2024
3a64aae
Fix typo in metadata explanation
noah-schechter Jul 1, 2024
4344e61
Merge pull request #1015 from atlosdotorg/noah-schechter-patch-2
milesmcc Jul 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions platform/assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,16 @@
}
}

/* Hide if there's a preceding sibling with data-sortable-id */
[data-sortable-id] ~ .sibling-sortable-hidden {
display: none;
}

/* Hide if there's a following sibling with data-sortable-id */
.sibling-sortable-hidden:has(~ [data-sortable-id]) {
display: none;
}

/**
Important CSS for our custom full-page map styling.,
*/
Expand Down
29 changes: 29 additions & 0 deletions platform/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
import Sortable from 'sortablejs';
import * as vega from "vega";
import "vega-lite"
import vegaEmbed from "vega-embed"
Expand Down Expand Up @@ -66,6 +67,34 @@ Hooks.ScrollToTop = {
}
}
Hooks.InfiniteScroll = InfiniteScroll;
Hooks.Sortable = {
mounted() {
// To use the sorter, create an element with `data-list_id` and
// `data-list_group`; then add a `data-sortable-id` to each of the things
// that you want to be draggable.
let sorter = new Sortable(this.el, {
animation: 150,
delay: 100,
handle: ".handle",
sort: this.el.getAttribute("data-sortable") !== "false",
dragClass: "drag-item",
ghostClass: "drag-ghost",
group: this.el.getAttribute("data-list_group"),
forceFallback: true,
onEnd: e => {
// Determine the new ordering of all elements in the group
let groupId = e.item.parentElement.dataset["list_id"];
let newOrderingElements = e.item.parentElement.querySelectorAll(":scope > [data-sortable-id]")
let newOrdering = Array.from(newOrderingElements).map((elem) => elem.dataset.sortableId);
let params = { old: e.oldIndex, new: e.newIndex, id: e.item.dataset["sortableId"], ordering: newOrdering, group: groupId }
this.pushEventTo(this.el, "reposition", params)
},
onMove(e) {
return !e.related.hasAttribute("data-sortable-fixed")
}
})
}
}

// Used by the pagination button to scroll back up to the top of the page.
window.scrollToTop = () => {
Expand Down
6 changes: 6 additions & 0 deletions platform/assets/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions platform/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"mapbox-gl": "^3.0.0",
"maplibre-gl": "^4.3.2",
"mark.js": "^8.11.1",
"sortablejs": "^1.15.2",
"tippy.js": "^6.3.7",
"tom-select": "^2.0.1",
"vega": "^5.22.1",
Expand Down
8 changes: 6 additions & 2 deletions platform/assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ module.exports = {
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('a17t'),
plugin(function({ addVariant }) {
plugin(function ({ addVariant }) {
addVariant('processing', '.processing &', 'processing')
})
}),
plugin(({ addVariant }) => addVariant("drag-item", [".drag-item&", ".drag-item &"])),
plugin(({ addVariant }) => addVariant("drag-ghost", [".drag-ghost&", ".drag-ghost &"])),
// Useful for hiding placeholders when dragging; active when there is a sibling with `data-sortable-id` set
plugin(({ addVariant }) => addVariant("sibling-sortable", ["~ [data-sortable-id] ~ &", "~ [data-sortable-id] &"])),
]
};
89 changes: 73 additions & 16 deletions platform/lib/platform/material/attribute.ex
Original file line number Diff line number Diff line change
Expand Up @@ -493,25 +493,57 @@ defmodule Platform.Material.Attribute do
attributes(opts) |> Enum.filter(&(&1.deprecated != true))
end

def filter_attributes_to_group(attrs, group, project) do
if is_nil(project) do
raise ArgumentError, "group cannot be filtered without project"
end

case group do
%Platform.Projects.ProjectAttributeGroup{member_ids: ids} ->
Enum.filter(attrs, fn a -> Enum.member?(ids, a.name) end)

:core ->
Enum.filter(attrs, fn a -> is_atom(a.name) end)

:unassigned ->
Enum.filter(attrs, fn a ->
not is_atom(a.name) and
Enum.all?(project.attribute_groups, fn g -> not Enum.member?(g.member_ids, a.name) end)
end)
end
end

@doc """
Get the names of the attributes that are available for the given media. Both nil and the empty list count as unset.

If the :pane option is given, only attributes in that pane will be returned. If include_deprecated_attributes is true, deprecated attributes will be included.
"""
def set_for_media(media, opts \\ []) do
pane = Keyword.get(opts, :pane)
groups = Keyword.get(opts, :groups)
project = Keyword.get(opts, :project)

Enum.filter(attributes(opts), fn a ->
all_attrs = get_children(a.name) ++ [a]
attrs =
Enum.filter(attributes(opts), fn a ->
all_attrs = get_children(a.name) ++ [a]

Enum.any?(all_attrs, fn attr ->
val = Material.get_attribute_value(media, attr)
Enum.any?(all_attrs, fn attr ->
val = Material.get_attribute_value(media, attr)

val != nil && val != [] && val != %{"day" => "", "month" => "", "year" => ""} &&
(pane == nil || attr.pane == pane || (attr.parent == a.name && a.pane == pane)) &&
(attr.deprecated != true || Keyword.get(opts, :include_deprecated_attributes, false))
val != nil && val != [] && val != %{"day" => "", "month" => "", "year" => ""} &&
(pane == nil || attr.pane == pane || (attr.parent == a.name && a.pane == pane)) &&
(attr.deprecated != true || Keyword.get(opts, :include_deprecated_attributes, false))
end)
end)
end)

# Now, if group and project are both set, we need to filter out the attributes that are not in the group.
if not is_nil(groups) do
Enum.flat_map(groups, fn group ->
filter_attributes_to_group(attrs, group, project)
end)
else
attrs
end
end

@doc """
Expand All @@ -522,13 +554,25 @@ defmodule Platform.Material.Attribute do
def unset_for_media(media, opts \\ []) do
pane = Keyword.get(opts, :pane)
set = set_for_media(media, opts)
groups = Keyword.get(opts, :groups)
project = Keyword.get(opts, :project)

attrs =
attributes(opts)
|> Enum.filter(
&(!Enum.member?(set, &1) &&
&1.deprecated != true &&
(pane == nil || &1.pane == pane))
)

attributes(opts)
|> Enum.filter(
&(!Enum.member?(set, &1) &&
&1.deprecated != true &&
(pane == nil || &1.pane == pane))
)
# Now, if group and project are both set, we need to filter out the attributes that are not in the group.
if not is_nil(groups) do
Enum.flat_map(groups, fn group ->
filter_attributes_to_group(attrs, group, project)
end)
else
attrs
end
end

@doc """
Expand Down Expand Up @@ -1183,8 +1227,21 @@ defmodule Platform.Material.Attribute do
defp verify_change_exists(changeset, attributes) do
# Verify that at least one of the given attributes has changed. This is used
# to ensure that users don't post updates that don't actually change anything.

if Enum.any?(attributes, &Map.has_key?(changeset.changes, &1.schema_field)) do
if Enum.any?(attributes, fn attribute ->
case Map.get(changeset.changes, attribute.schema_field) do
nil ->
false

changes when attribute.schema_field == :project_attributes ->
# For project_attributes, check if any sub-changeset has actual changes
Enum.any?(changes, fn sub_changeset ->
map_size(sub_changeset.changes) > 0 and Map.has_key?(sub_changeset.changes, :value)
end)

_changes ->
true
end
end) do
changeset
else
changeset
Expand Down
40 changes: 39 additions & 1 deletion platform/lib/platform/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Platform.Projects do
"""

import Ecto.Query, warn: false
alias Platform.Projects.ProjectAttribute
alias Platform.Projects.{ProjectAttribute, ProjectAttributeGroup}
alias Platform.Repo

alias Platform.Projects.Project
Expand Down Expand Up @@ -215,6 +215,44 @@ defmodule Platform.Projects do
|> Repo.update()
end

@doc """
Deletes an embedded custom project attribute group. Checks user permission.

## Examples

iex> delete_project_attribute_group(project, "existing_id")
{:ok, %Project{}}

iex> delete_project_attribute_group(project, "non_existing_id")
{:error, %Ecto.Changeset{}}
"""
def delete_project_attribute_group(%Project{} = project, id, user \\ nil) do
# Verify the user has permission to edit the project
unless is_nil(user) || Permissions.can_edit_project_metadata?(user, project) do
raise "User does not have permission to edit this project"
end

# Verify the attribute exists
unless Enum.any?(project.attribute_groups, fn g -> g.id == id end) do
raise "Attribute does not exist"
end

# Delete the attribute
change_project(project)
|> Ecto.Changeset.put_embed(
:attribute_groups,
project.attribute_groups
|> Enum.map(fn g ->
if g.id == id do
ProjectAttributeGroup.changeset(g) |> Map.put(:action, :delete)
else
g
end
end)
)
|> Repo.update()
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking project changes.

Expand Down
4 changes: 3 additions & 1 deletion platform/lib/platform/projects/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Platform.Projects.Project do
field(:should_sync_with_internet_archive, :boolean, default: false)

embeds_many(:attributes, Platform.Projects.ProjectAttribute, on_replace: :delete)
embeds_many(:attribute_groups, Platform.Projects.ProjectAttributeGroup, on_replace: :delete)

has_many(:media, Platform.Material.Media)
has_many(:memberships, Platform.Projects.ProjectMembership)
Expand All @@ -29,7 +30,8 @@ defmodule Platform.Projects.Project do
def changeset(project, attrs) do
project
|> cast(attrs, [:name, :code, :color, :description, :should_sync_with_internet_archive])
|> cast_embed(:attributes, required: false)
|> cast_embed(:attributes, required: false, sort_param: :position)
|> cast_embed(:attribute_groups, required: false, sort_param: :position)
|> validate_required([:name, :code, :color])
|> then(fn changeset ->
changeset
Expand Down
12 changes: 11 additions & 1 deletion platform/lib/platform/projects/project_attribute.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Platform.Projects.ProjectAttribute do
# empty string if not a decorator
field(:decorator_for, :string, default: "")
field(:enabled, :boolean, default: true)
field(:ordering, :integer, default: 0)

# JSON array of options
field(:options_json, :string, virtual: true)
Expand Down Expand Up @@ -41,7 +42,16 @@ defmodule Platform.Projects.ProjectAttribute do
|> then(&if &1 == "", do: Jason.encode!(attribute.options), else: &1)

attribute
|> cast(attrs, [:name, :type, :options_json, :id, :description, :decorator_for, :enabled])
|> cast(attrs, [
:name,
:ordering,
:type,
:options_json,
:id,
:description,
:decorator_for,
:enabled
])
|> cast(%{options_json: json_options}, [:options_json])
|> cast(
%{options: Jason.decode!(json_options)},
Expand Down
30 changes: 30 additions & 0 deletions platform/lib/platform/projects/project_attribute_group.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Platform.Projects.ProjectAttributeGroup do
use Ecto.Schema
import Ecto.Changeset

@derive {Jason.Encoder, only: [:name]}
@primary_key {:id, :binary_id, autogenerate: true}
embedded_schema do
field(:name, :string)
field(:description, :string, default: "")
field(:color, :string, default: "#808080")
field(:show_in_creation_form, :boolean, default: true)
# These can be a mix of binary IDs and string attributes, for core vs custom attributes
# Source of truth for membership but not ordering
field(:member_ids, {:array, :string}, default: [])
field(:ordering, :integer, default: 0)
end

@doc """
Changeset for project attribute groups.
"""
def changeset(%__MODULE__{} = attribute, attrs \\ %{}) do
attribute
|> cast(attrs, [:name, :show_in_creation_form, :member_ids, :description, :ordering, :color])
# Validate that "color" matches a hex color code via regex
|> validate_format(:color, ~r/^#[0-9a-fA-F]{6}$/)
|> validate_length(:name, min: 1, max: 240)
|> validate_length(:description, max: 3000)
|> validate_required(:name)
end
end
19 changes: 12 additions & 7 deletions platform/lib/platform/workers/archiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,23 @@ defmodule Platform.Workers.Archiver do
end)
|> Enum.filter(&(&1 != :skip))

# Update the media version
version_map = %{
status: :complete,
# Append the artifacts; some may already exist
artifacts: Enum.map(version.artifacts || [], &Map.from_struct(&1)) ++ artifacts,
metadata: %{
# Combine the old metadata and the new metadata (we don't want to override!)
# TODO: Move these into their own internal namespace
combined_metadata =
Map.merge(version.metadata || %{}, %{
auto_archive_successful: Map.get(metadata, "auto_archive_successful", false),
crawl_successful: Map.get(metadata, "crawl_successful", false),
page_info: Map.get(metadata, "page_info"),
content_info: Map.get(metadata, "content_info"),
is_likely_authwalled: Map.get(metadata, "is_likely_authwalled", false)
}
})

# Update the media version
version_map = %{
status: :complete,
# Append the artifacts; some may already exist
artifacts: Enum.map(version.artifacts || [], &Map.from_struct(&1)) ++ artifacts,
metadata: combined_metadata
}

{:ok, version} = Material.update_media_version(version, version_map)
Expand Down
Loading
Loading