Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/ci-waydowntown-full-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
api-level: 24
arch: x86_64
profile: Nexus 6
script: flutter test integration_test/token_refresh_test.dart integration_test/string_collector_test.dart integration_test/fill_in_the_blank_test.dart integration_test/team_game_test.dart integration_test/edit_specification_test.dart --flavor local --dart-define=API_BASE_URL=http://10.0.2.2:4001 --timeout none --file-reporter json:test-results.json
script: flutter test integration_test/token_refresh_test.dart integration_test/string_collector_test.dart integration_test/fill_in_the_blank_test.dart integration_test/team_game_test.dart integration_test/edit_specification_test.dart integration_test/validation_test.dart --flavor local --dart-define=API_BASE_URL=http://10.0.2.2:4001 --timeout none --file-reporter json:test-results.json

- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
Expand Down
65 changes: 65 additions & 0 deletions registrations/lib/registrations/accounts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule Registrations.Accounts do
@moduledoc false
import Ecto.Query, warn: false

alias Registrations.Repo
alias Registrations.UserRole

def list_user_roles(user) do
from(r in UserRole, where: r.user_id == ^user.id)
|> Repo.all()
|> Repo.preload([:user, :assigned_by])
end

def list_all_user_roles(filters \\ %{}) do
UserRole
|> filter_roles_query(filters)
|> Repo.all()
|> Repo.preload([:user, :assigned_by])
end

defp filter_roles_query(query, filters) do
Enum.reduce(filters, query, fn
{"role", role}, query when is_binary(role) ->
from(r in query, where: r.role == ^role)

_, query ->
query
end)
end

def get_user_role!(id) do
UserRole
|> Repo.get!(id)
|> Repo.preload([:user, :assigned_by])
end

def assign_role(user_id, role, assigned_by_id \\ nil) do
%UserRole{}
|> UserRole.changeset(%{user_id: user_id, role: role, assigned_by_id: assigned_by_id})
|> Repo.insert()
|> case do
{:ok, user_role} -> {:ok, Repo.preload(user_role, [:user, :assigned_by])}
error -> error
end
end

def remove_role(id) do
UserRole
|> Repo.get!(id)
|> Repo.delete()
end

def has_role?(user, role) do
Repo.exists?(from(r in UserRole, where: r.user_id == ^user.id and r.role == ^role))
end

def list_users_with_role(role) do
from(r in UserRole,
where: r.role == ^role,
preload: [:user]
)
|> Repo.all()
|> Enum.map(& &1.user)
end
end
30 changes: 30 additions & 0 deletions registrations/lib/registrations/user_role.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Registrations.UserRole do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset

@primary_key {:id, :binary_id, autogenerate: true}

@valid_roles ~w(validator validation_overseer)

schema "user_roles" do
field(:role, :string)

belongs_to(:user, RegistrationsWeb.User, type: :binary_id)
belongs_to(:assigned_by, RegistrationsWeb.User, type: :binary_id, foreign_key: :assigned_by_id)

timestamps()
end

@doc false
def changeset(user_role, attrs) do
user_role
|> cast(attrs, [:user_id, :role, :assigned_by_id])
|> validate_required([:user_id, :role])
|> validate_inclusion(:role, @valid_roles)
|> unique_constraint([:user_id, :role])
|> assoc_constraint(:user)
end

def valid_roles, do: @valid_roles
end
105 changes: 105 additions & 0 deletions registrations/lib/registrations/waydowntown.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ defmodule Registrations.Waydowntown do
alias Registrations.Waydowntown.Reveal
alias Registrations.Waydowntown.Run
alias Registrations.Waydowntown.Specification
alias Registrations.Waydowntown.SpecificationValidation
alias Registrations.Waydowntown.Submission
alias Registrations.Waydowntown.ValidationComment
alias RegistrationsWeb.User

def update_user(user, attrs) do
Expand Down Expand Up @@ -904,4 +906,107 @@ defmodule Registrations.Waydowntown do
|> Repo.get!(id)
|> Repo.preload([:answer, :user])
end

# Specification Validations

defp validation_preloads do
user_query = user_preload_query()

[
specification: [answers: [:region], region: [parent: [parent: [:parent]]]],
validation_comments: [:answer],
validator: user_query,
assigned_by: user_query,
run: []
]
end

def create_specification_validation(attrs) do
%SpecificationValidation{}
|> SpecificationValidation.changeset(attrs)
|> Repo.insert()
|> case do
{:ok, validation} -> {:ok, Repo.preload(validation, validation_preloads())}
error -> error
end
end

def get_specification_validation!(id) do
SpecificationValidation
|> Repo.get!(id)
|> Repo.preload(validation_preloads())
end

def update_specification_validation(%SpecificationValidation{} = validation, attrs, role) do
validation
|> SpecificationValidation.changeset(attrs)
|> SpecificationValidation.validate_status_transition(validation.status, role)
|> Repo.update()
|> case do
{:ok, validation} -> {:ok, Repo.preload(validation, validation_preloads())}
error -> error
end
end

def list_validations_for_validator(user) do
from(v in SpecificationValidation, where: v.validator_id == ^user.id, order_by: [desc: v.inserted_at])
|> Repo.all()
|> Repo.preload(validation_preloads())
end

def list_validations_for_overseer(user) do
from(v in SpecificationValidation, where: v.assigned_by_id == ^user.id, order_by: [desc: v.inserted_at])
|> Repo.all()
|> Repo.preload(validation_preloads())
end

def list_validations_for_specification(specification_id) do
from(v in SpecificationValidation, where: v.specification_id == ^specification_id, order_by: [desc: v.inserted_at])
|> Repo.all()
|> Repo.preload(validation_preloads())
end

def list_specification_validations do
SpecificationValidation
|> Repo.all()
|> Repo.preload(validation_preloads())
end

# Validation Comments

def create_validation_comment(attrs) do
%ValidationComment{}
|> ValidationComment.changeset(attrs)
|> Repo.insert()
|> case do
{:ok, comment} -> {:ok, Repo.preload(comment, [:answer, :specification_validation])}
error -> error
end
end

def get_validation_comment!(id) do
ValidationComment
|> Repo.get!(id)
|> Repo.preload([:answer, :specification_validation])
end

def update_validation_comment(%ValidationComment{} = comment, attrs) do
comment
|> ValidationComment.changeset(attrs)
|> Repo.update()
|> case do
{:ok, comment} -> {:ok, Repo.preload(comment, [:answer, :specification_validation])}
error -> error
end
end

def delete_validation_comment(%ValidationComment{} = comment) do
Repo.delete(comment)
end

def list_validation_comments(validation_id) do
from(c in ValidationComment, where: c.specification_validation_id == ^validation_id)
|> Repo.all()
|> Repo.preload([:answer, :specification_validation])
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule Registrations.Waydowntown.SpecificationValidation do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset

@primary_key {:id, :binary_id, autogenerate: true}
@schema_prefix "waydowntown"

@valid_statuses ~w(assigned in_progress submitted accepted rejected)
@validator_transitions %{
"assigned" => ["in_progress"],
"in_progress" => ["submitted"]
}
@overseer_transitions %{
"submitted" => ["accepted", "rejected"]
}

schema "specification_validations" do
field(:status, :string, default: "assigned")
field(:play_mode, :string)
field(:overall_notes, :string)

belongs_to(:specification, Registrations.Waydowntown.Specification, type: :binary_id)
belongs_to(:validator, RegistrationsWeb.User, type: :binary_id, foreign_key: :validator_id)
belongs_to(:assigned_by, RegistrationsWeb.User, type: :binary_id, foreign_key: :assigned_by_id)
belongs_to(:run, Registrations.Waydowntown.Run, type: :binary_id)

has_many(:validation_comments, Registrations.Waydowntown.ValidationComment, on_delete: :delete_all)

timestamps()
end

@doc false
def changeset(validation, attrs) do
validation
|> cast(attrs, [:specification_id, :validator_id, :assigned_by_id, :status, :play_mode, :overall_notes, :run_id])
|> validate_required([:specification_id, :validator_id, :assigned_by_id])
|> validate_inclusion(:status, @valid_statuses)
|> validate_inclusion(:play_mode, ~w(blind with_answers), message: "must be 'blind' or 'with_answers'")
|> unique_constraint([:specification_id, :validator_id])
|> assoc_constraint(:specification)
end

def validate_status_transition(changeset, current_status, role) do
new_status = get_change(changeset, :status)

if new_status do
transitions =
case role do
:validator -> @validator_transitions
:overseer -> @overseer_transitions
end

allowed = Map.get(transitions, current_status, [])

if new_status in allowed do
changeset
else
add_error(changeset, :status, "cannot transition from '#{current_status}' to '#{new_status}'")
end
else
changeset
end
end

def valid_statuses, do: @valid_statuses
end
42 changes: 42 additions & 0 deletions registrations/lib/registrations/waydowntown/validation_comment.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Registrations.Waydowntown.ValidationComment do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset

@primary_key {:id, :binary_id, autogenerate: true}
@schema_prefix "waydowntown"

@valid_fields ~w(answer label hint)

schema "validation_comments" do
field(:field, :string)
field(:comment, :string)
field(:suggested_value, :string)

belongs_to(:specification_validation, Registrations.Waydowntown.SpecificationValidation, type: :binary_id)
belongs_to(:answer, Registrations.Waydowntown.Answer, type: :binary_id)

timestamps()
end

@doc false
def changeset(comment, attrs) do
comment
|> cast(attrs, [:specification_validation_id, :answer_id, :field, :comment, :suggested_value])
|> validate_required([:specification_validation_id])
|> validate_inclusion(:field, @valid_fields)
|> validate_has_content()
|> assoc_constraint(:specification_validation)
end

defp validate_has_content(changeset) do
comment = get_field(changeset, :comment)
suggested_value = get_field(changeset, :suggested_value)

if is_nil(comment) and is_nil(suggested_value) do
add_error(changeset, :comment, "at least one of comment or suggested_value is required")
else
changeset
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ defmodule RegistrationsWeb.SessionController do

# FIXME can this be tested? It uses HTTPOnly cookie
def show(conn, params) do
user = conn.assigns[:current_user]
user = Registrations.Repo.preload(user, :user_roles)
conn = assign(conn, :current_user, user)
render(conn, "show.json", %{conn: conn, params: params})
end

Expand Down
Loading
Loading