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

feat: activity enrollments #477

Merged
merged 11 commits into from
Feb 4, 2025
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
2 changes: 2 additions & 0 deletions lib/safira/accounts/attendee.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ defmodule Safira.Accounts.Attendee do
belongs_to :course, Safira.Accounts.Course
belongs_to :user, Safira.Accounts.User

has_many :enrolments, Safira.Activities.Enrolment

timestamps(type: :utc_datetime)
end

Expand Down
1 change: 1 addition & 0 deletions lib/safira/accounts/roles/permissions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Safira.Accounts.Roles.Permissions do
"staffs" => ["show", "edit", "roles_edit"],
"challenges" => ["show", "edit", "delete"],
"companies" => ["edit"],
"enrolments" => ["show", "edit"],
"products" => ["show", "edit", "delete"],
"purchases" => ["show", "redeem", "refund"],
"badges" => ["show", "edit", "delete", "give", "revoke", "give_without_restrictions"],
Expand Down
93 changes: 91 additions & 2 deletions lib/safira/activities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Safira.Activities do

use Safira.Context

alias Safira.Activities.{Activity, ActivityCategory, Speaker}
alias Safira.Activities.{Activity, ActivityCategory, Enrolment, Speaker}

@doc """
Returns the list of activities.
Expand Down Expand Up @@ -89,7 +89,7 @@ defmodule Safira.Activities do
"""
def get_activity!(id) do
Activity
|> preload(:speakers)
|> preload([:speakers, enrolments: [attendee: [:user]]])
|> Repo.get!(id)
end

Expand Down Expand Up @@ -439,4 +439,93 @@ defmodule Safira.Activities do
|> Repo.all()
|> Repo.preload(:activities)
end

@doc """
Returns the list of enrolments of an attendee.

## Examples

iex> get_attendee_enrolments(attendee_id)
[%Enrolment{}, ...]

"""
def get_attendee_enrolments(attendee_id) do
Enrolment
|> where([e], e.attendee_id == ^attendee_id)
|> preload(:activity)
|> Repo.all()
end

@doc """
Enrols an attendee in an activity

## Examples

iex> enrol(attendee_id, activity_id)
{:ok, %{enrolment: %Enrolment{}, activity: %Activity{}, new_activity: %Activity{}}}

iex> enrol(attendee_id, activity_id)
{:error, :struct, %Ecto.Changeset{}, %{}}
"""
def enrol(attendee_id, activity_id) do
Ecto.Multi.new()
# We need to read the activity before updating the enrolment count to avoid
# a race condition where the enrolment count changes after the activity was last
# read from the database, and before this transaction began
|> Ecto.Multi.one(:activity, Activity |> where([a], a.id == ^activity_id))
|> Ecto.Multi.insert(
:enrolment,
Enrolment.changeset(
%Enrolment{},
%{
attendee_id: attendee_id,
activity_id: activity_id
}
)
)
|> Ecto.Multi.update(:new_activity, fn %{activity: act} ->
Activity.changeset(act, %{enrolment_count: act.enrolment_count + 1})
end)
|> Repo.transaction()
end

@doc """
Deletes an enrolment

## Examples

iex> unenrol(attendee_id, activity_id)
{:ok, %{enrolment: %Enrolment{}, activity: %Activity{}, new_activity: %Activity{}}}

iex> unenrol(attendee_id, activity_id)
{:error, :struct, %Ecto.Changeset{}, %{}}
"""
def unenrol(enrolment) do
Ecto.Multi.new()
# We need to read the activity before updating the enrolment count to avoid
# a race condition where the enrolment count changes after the activity was last
# read from the database, and before this transaction began
|> Ecto.Multi.one(:activity, Activity |> where([a], a.id == ^enrolment.activity_id))
|> Ecto.Multi.delete(
:enrolment,
enrolment
)
|> Ecto.Multi.update(:new_activity, fn %{activity: act} ->
Activity.changeset(act, %{enrolment_count: act.enrolment_count - 1})
end)
|> Repo.transaction()
end

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

## Examples

iex> change_enrolment(enrolment)
%Ecto.Changeset{data: %Enrolment{}}

"""
def change_enrolment(%Enrolment{} = enrolment, attrs \\ %{}) do
Enrolment.changeset(enrolment, attrs)
end
end
9 changes: 8 additions & 1 deletion lib/safira/activities/activity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Safira.Activities.Activity do
alias Safira.Event

@required_fields ~w(title date time_start time_end)a
@optional_fields ~w(description category_id location has_enrolments max_enrolments)a
@optional_fields ~w(description category_id location has_enrolments max_enrolments enrolment_count)a

@derive {
Flop.Schema,
Expand All @@ -34,13 +34,16 @@ defmodule Safira.Activities.Activity do
field :time_end, :time
field :has_enrolments, :boolean, default: false
field :max_enrolments, :integer, default: 0
field :enrolment_count, :integer, default: 0

belongs_to :category, Safira.Activities.ActivityCategory

many_to_many :speakers, Safira.Activities.Speaker,
join_through: "activities_speakers",
on_replace: :delete

has_many :enrolments, Safira.Activities.Enrolment

timestamps(type: :utc_datetime)
end

Expand All @@ -51,6 +54,10 @@ defmodule Safira.Activities.Activity do
|> validate_required(@required_fields)
|> validate_activity_date()
|> validate_activity_times()
|> check_constraint(:max_enrolments,
name: :activity_not_overbooked,
message: "Activity overbooked"
)
end

@doc false
Expand Down
26 changes: 26 additions & 0 deletions lib/safira/activities/enrolment.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Safira.Activities.Enrolment do
@moduledoc """
Enrollments for activities.
"""

use Safira.Schema

alias Safira.Accounts.Attendee
alias Safira.Activities.Activity

@required_fields ~w(attendee_id activity_id)a

schema "enrolments" do
belongs_to :attendee, Attendee
belongs_to :activity, Activity

timestamps(type: :utc_datetime)
end

def changeset(enrolment, attrs) do
enrolment
|> cast(attrs, @required_fields)
|> validate_required(@required_fields)
|> unique_constraint([:activity_id, :attendee_id])
end
end
4 changes: 3 additions & 1 deletion lib/safira_web/components/forms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ defmodule SafiraWeb.Components.Forms do
attr :field, HTML.FormField,
doc: "A form field struct retrieved from the form, for example: `@form[:email]`."

attr :mode, :atom, default: :single, doc: "The mode of the multi select."

attr :rest, :global,
include: ~w(value_mapper placeholder),
doc: "Any other attribute to be added to the input."
Expand All @@ -525,7 +527,7 @@ defmodule SafiraWeb.Components.Forms do

<.live_select
id={assigns.id || @field.id}
mode={:tags}
mode={@mode}
field={@field}
phx-target={@target}
container_class={"#{@wrapper_class}"}
Expand Down
32 changes: 29 additions & 3 deletions lib/safira_web/controllers/user_session_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,29 @@ defmodule SafiraWeb.UserSessionController do
create(conn, params, "Welcome back!")
end

defp create(conn, %{"user" => user_params}, info) do
defp create(
conn,
%{
"user" => user_params
} = params,
info
) do
%{"email" => email, "password" => password} = user_params

action = Map.get(params, "action")
action_id = Map.get(params, "action_id")
return_to = Map.get(params, "return_to")

if user = Accounts.get_user_by_email_and_password(email, password) do
conn
|> put_flash(:info, info)
|> process_action(action, action_id, user, return_to, info)
|> UserAuth.log_in_user(user, user_params)
else
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
conn
|> put_flash(:error, "Invalid email or password")
|> put_flash(:email, String.slice(email, 0, 160))
|> redirect(to: ~p"/users/log_in")
|> redirect(to: conn.request_path)
end
end

Expand All @@ -56,4 +66,20 @@ defmodule SafiraWeb.UserSessionController do
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end

defp process_action(conn, "enrol", id, user, return_to, _info) do
attendee = Safira.Accounts.get_user_attendee(user.id)

case Safira.Activities.enrol(attendee.id, id) do
{:ok, _} ->
put_flash(conn, :info, "Successfully enrolled")
|> redirect(to: return_to)

{:error, _, _, _} ->
put_flash(conn, :error, gettext("Unable to enrol"))
end
end

defp process_action(conn, _action, _id, _user, _return_to, info),
do: put_flash(conn, :info, info)
end
19 changes: 15 additions & 4 deletions lib/safira_web/live/auth/user_login_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ defmodule SafiraWeb.UserLoginLive do
</:subtitle>
</.header>

<.simple_form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore">
<.simple_form
for={@form}
id="login_form"
action={
~p"/users/log_in?action=#{@action || ""}&action_id=#{@action_id || ""}&return_to=#{@return_to || ""}"
}
phx-update="ignore"
>
<.input field={@form[:email]} type="email" label="Email" required />
<.input field={@form[:password]} type="password" label="Password" required />

Expand All @@ -50,11 +57,15 @@ defmodule SafiraWeb.UserLoginLive do
"""
end

def mount(_params, _session, socket) do
def mount(params, _session, socket) do
email = Phoenix.Flash.get(socket.assigns.flash, :email)
form = to_form(%{"email" => email}, as: "user")

{:ok, assign(socket, form: form) |> assign(registrations_open: Event.registrations_open?()),
temporary_assigns: [form: form]}
{:ok,
assign(socket, form: form)
|> assign(registrations_open: Event.registrations_open?())
|> assign(:action_id, Map.get(params, "action_id"))
|> assign(:action, Map.get(params, "action"))
|> assign(:return_to, Map.get(params, "return_to")), temporary_assigns: [form: form]}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ defmodule SafiraWeb.Backoffice.ScheduleLive.ActivityLive.FormComponent do
wrapper_class="w-full"
/>
<.field_multiselect
mode={:tags}
id="speakers"
field={@form[:speakers]}
target={@myself}
Expand Down
Loading