Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches:
- stable
- master
- multitenant-deploy
paths:
- 'config/**'
- 'lib/**'
Expand Down
2 changes: 1 addition & 1 deletion .iex.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import Ecto.Query
alias Cadet.Repo
alias Cadet.Accounts.User
alias Cadet.Assessments.{Answer, Assessment, Question, Submission}
alias Cadet.Course.Group
alias Cadet.Courses.Group
1 change: 1 addition & 0 deletions config/cadet.exs.example
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ config :cadet,
],
uploader: [
assets_bucket: "<unique-identifier>-assets",
assets_prefix: "courses/",
sourcecasts_bucket: "<unique-identifier>-cadet-sourcecasts"
],
# Configuration for Sling integration (executing on remote devices)
Expand Down
12 changes: 12 additions & 0 deletions config/dev.secrets.exs.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ config :cadet,
# # You may need to write your own claim extractor for other providers
# claim_extractor: Cadet.Auth.Providers.CognitoClaimExtractor
# }},
# # To use authentication with GitHub
# "github" =>
# {Cadet.Auth.Providers.GitHub,
# %{
# # A map of GitHub client_id => client_secret
# clients: %{
# "client_id" => "client_secret"
# },
# token_url: "https://github.com/login/oauth/access_token",
# user_api: "https://api.github.com/user"
# }},
"test" =>
{Cadet.Auth.Providers.Config,
[
Expand Down Expand Up @@ -67,6 +78,7 @@ config :cadet,
],
uploader: [
assets_bucket: "source-academy-assets",
assets_prefix: "courses-dev/",
sourcecasts_bucket: "env-cadet-sourcecasts"
],
# Configuration for Sling integration (executing on remote devices)
Expand Down
20 changes: 12 additions & 8 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,31 @@ config :cadet,
token: "admin_token",
code: "admin_code",
name: "Test Admin",
username: "admin",
role: :admin
username: "admin"
# role: :admin
},
%{
token: "staff_token",
code: "staff_code",
name: "Test Staff",
username: "staff",
role: :staff
username: "staff"
# role: :staff
},
%{
token: "student_token",
code: "student_code",
name: "Test Student",
username: "student",
role: :student
name: "student 1",
username: "E1234564"
# role: :student
}
]}
},
autograder: [
lambda_name: "dummy"
],
uploader: [
assets_bucket: "source-academy-assets",
assets_bucket: "test-sa-assets",
assets_prefix: "courses-test/",
sourcecasts_bucket: "test-cadet-sourcecasts"
],
remote_execution: [
Expand All @@ -85,3 +86,6 @@ config :cadet,
]

config :arc, storage: Arc.Storage.Local

if "test.secrets.exs" |> Path.expand(__DIR__) |> File.exists?(),
do: import_config("test.secrets.exs")
106 changes: 59 additions & 47 deletions lib/cadet/accounts/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,16 @@ defmodule Cadet.Accounts do

import Ecto.Query

alias Cadet.Accounts.{Query, User}
alias Cadet.Accounts.{Query, User, CourseRegistration}
alias Cadet.Auth.Provider

@doc """
Register new User entity using Cadet.Accounts.Form.Registration

Returns {:ok, user} on success, otherwise {:error, changeset}
"""
def register(attrs = %{username: username}, role) when is_binary(username) do
attrs |> Map.put(:role, role) |> insert_or_update_user()
end

@doc """
Creates User entity with specified attributes.
"""
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
def register(attrs = %{username: username}) when is_binary(username) do
attrs |> insert_or_update_user()
end

@doc """
Expand Down Expand Up @@ -53,58 +44,79 @@ defmodule Cadet.Accounts do
Repo.get(User, id)
end

@get_all_role ~w(admin staff)a
@doc """
Returns users matching a given set of criteria.
"""
def get_users(filter \\ []) do
User
|> join(:left, [u], g in assoc(u, :group))
|> preload([u, g], group: g)
|> get_users(filter)
def get_users_by(filter \\ [], %CourseRegistration{course_id: course_id, role: role})
when role in @get_all_role do
CourseRegistration
|> where([cr], cr.course_id == ^course_id)
|> join(:inner, [cr], u in assoc(cr, :user))
|> preload([cr, u], user: u)
|> join(:left, [cr, u], g in assoc(cr, :group))
|> preload([cr, u, g], group: g)
|> get_users_helper(filter)
end

defp get_users(query, []), do: Repo.all(query)
defp get_users_helper(query, []), do: Repo.all(query)

defp get_users(query, [{:group, group} | filters]),
do: query |> where([u, g], g.name == ^group) |> get_users(filters)
defp get_users_helper(query, [{:group, group} | filters]),
do: query |> where([cr, u, g], g.name == ^group) |> get_users_helper(filters)

defp get_users(query, [filter | filters]), do: query |> where(^[filter]) |> get_users(filters)
defp get_users_helper(query, [filter | filters]),
do: query |> where(^[filter]) |> get_users_helper(filters)

@spec sign_in(String.t(), Provider.token(), Provider.provider_instance()) ::
{:error, :bad_request | :forbidden | :internal_server_error, String.t()} | {:ok, any}
@doc """
Sign in using given user ID
"""
def sign_in(username, token, provider) do
case Repo.one(Query.username(username)) do
nil ->
# user is not registered in our database
with {:ok, role} <- Provider.get_role(provider, token),
{:ok, name} <- Provider.get_name(provider, token),
{:ok, _} <- register(%{name: name, username: username}, role) do
sign_in(username, name, token)
else
{:error, :invalid_credentials, err} ->
{:error, :forbidden, err}

{:error, :upstream, err} ->
{:error, :bad_request, err}

{:error, _err} ->
{:error, :internal_server_error}
end

user ->
{:ok, user}
user = username |> Query.username() |> Repo.one()

if is_nil(user) or is_nil(user.name) do
# user is not registered in our database or does not have a name
# (accounts pre-created by instructors do not have a name, and has to be fetched
# from the auth provider during sign_in)
with {:ok, name} <- Provider.get_name(provider, token),
{:ok, _} <- register(%{name: name, username: username}) do
sign_in(username, name, token)
else
{:error, :invalid_credentials, err} ->
{:error, :forbidden, err}

{:error, :upstream, err} ->
{:error, :bad_request, err}

{:error, _err} ->
{:error, :internal_server_error}
end
else
{:ok, user}
end
end

def update_game_states(user = %User{}, new_game_state = %{}) do
case user
|> User.changeset(%{game_states: new_game_state})
|> Repo.update() do
result = {:ok, _} -> result
{:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}}
def update_latest_viewed(user = %User{id: user_id}, latest_viewed_course_id)
when is_ecto_id(latest_viewed_course_id) do
CourseRegistration
|> where(user_id: ^user_id)
|> where(course_id: ^latest_viewed_course_id)
|> Repo.one()
|> case do
nil ->
{:error, {:bad_request, "user is not in the course"}}

_ ->
case user
|> User.changeset(%{latest_viewed_course_id: latest_viewed_course_id})
|> Repo.update() do
result = {:ok, _} ->
result

{:error, changeset} ->
{:error, {:internal_server_error, full_error_messages(changeset)}}
end
end
end
end
32 changes: 32 additions & 0 deletions lib/cadet/accounts/course_registration.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Cadet.Accounts.CourseRegistration do
@moduledoc """
The mapping table representing the registration of a user to a course.
"""
use Cadet, :model

alias Cadet.Accounts.{Role, User}
alias Cadet.Courses.{Course, Group}

schema "course_registrations" do
field(:role, Role)
field(:game_states, :map)
field(:agreed_to_research, :boolean)

belongs_to(:group, Group)
belongs_to(:user, User)
belongs_to(:course, Course)

timestamps()
end

@required_fields ~w(user_id course_id role)a
@optional_fields ~w(game_states group_id agreed_to_research)a

def changeset(course_registration, params \\ %{}) do
course_registration
|> cast(params, @optional_fields ++ @required_fields)
|> add_belongs_to_id_from_model([:user, :group, :course], params)
|> validate_required(@required_fields)
|> unique_constraint(:user_id, name: :course_registrations_user_id_course_id_index)
end
end
Loading