Current section

Files

Jump to
ami lib ami enrollment.ex
Raw

lib/ami/enrollment.ex

defmodule Ami.Enrollment do
use Ecto.Schema
use Timex
import Ecto.{
Query,
Changeset
}
alias Ami.{
CoursePeriod,
Repo,
Score,
PackagePeriod,
Course,
Unit,
Quiz,
Buddyship,
UserAnswer,
User,
Resource,
Enrollment,
CommunityUser,
Assignment,
Lesson
}
schema "enrollments" do
@timestamps_opts [type: :naive_datetime_usec]
field(:type, :string)
field(:enrollment_date, :utc_datetime_usec)
field(:created_at, :utc_datetime_usec)
field(:updated_at, :utc_datetime_usec)
field(:assignment_status, :string)
field(:active, :boolean)
field(:passed_on, :utc_datetime_usec)
field(:status, :integer)
field(:course_started, :boolean)
field(:course_completed, :boolean)
field(:assignment_score, :integer)
field(:assignment_sent_on, :utc_datetime_usec)
field(:assignment_reviewed_on, :date)
field(:progress, :binary)
field(:is_archived, :boolean)
belongs_to(:user, User)
belongs_to(:course_period, CoursePeriod)
belongs_to(:package_period, PackagePeriod)
belongs_to(:buddyship, Buddyship)
# user_id buddyship_id course_period_id
has_one(:buddy, through: [:buddyship, :buddy])
has_many(:assignments, Assignment, on_delete: :delete_all)
has_many(:scores, Score, on_delete: :delete_all)
end
def update_progress(enrollment) do
progress = calculate_progress(enrollment)
Ecto.Changeset.change(enrollment, %{progress: :erlang.term_to_binary(progress)})
|> Repo.update()
end
@doc """
Builds a changeset for saving assignment rating (aka assignment-score)
"""
def assignment_rating_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:assignment_score, :updated_at, :created_at])
end
@doc """
Builds a changeset creating a course enrollment
- validates presence of course_period_id and user_id foreign keys
- validates with course_period_id uniqueness scoped by user_id
- check if user belongs to course period's community when it's a community course period
"""
def course_period_changeset(enrollment_struct, params \\ %{}, %CoursePeriod{} = course_period) do
enrollment_struct
|> cast(params, [
:created_at,
:updated_at,
:user_id,
:course_period_id,
:enrollment_date,
:package_period_id,
:status,
:active
])
|> validate_required([:course_period_id, :user_id])
|> check_community_belonging(course_period)
|> unique_constraint(
:course_period_id,
name: :index_enrollments_on_course_period_id_and_user_id,
message: "already enrolled in this course"
)
end
defp check_community_belonging(changeset, %CoursePeriod{community_id: nil}) do
changeset
end
defp check_community_belonging(changeset, course_period) do
query =
from(
cu in CommunityUser,
where: cu.user_id == ^changeset.changes.user_id,
where: cu.community_id == ^course_period.community_id,
where: cu.status == 1,
select: count(cu.id)
)
# or Communities.Repo.one(query) but failing test, not sure how it impact performance though
count = Repo.one(query)
cond do
count > 0 ->
changeset
count == 0 ->
add_error(changeset, :community, "can't enroll, you don't belong to the community")
end
end
@doc """
Builds a changeset for creating a package enrollment with package_period_id uniqueness validation scoped by user_id
"""
def package_period_changeset(%Enrollment{} = enrollment, params \\ %{}) do
enrollment
|> cast(params, [
:created_at,
:updated_at,
:user_id,
:package_period_id,
:status,
:enrollment_date
])
|> validate_required([:package_period_id, :user_id])
|> unique_constraint(
:package_period_id,
name: :index_enrollments_on_package_period_id_and_user_id,
message: "already enrolled in this package"
)
end
@doc """
Pattern to fetch resources as lesson.resources with resourceable_type == "Enrollment"
"""
def preload_resources(struct), do: Resource.preload_resources(struct, "Enrollment")
def get_current_enrollments_for(user_struct) do
community_enrollments = get_community_enrollments_for(user_struct)
public_enrollments =
if user_struct.status in [0, 1], do: get_public_enrollments_for(user_struct), else: []
package_enrollments = get_package_enrollments_for(user_struct)
learninglab_enrollments = get_learninglab_enrollments_for(user_struct)
%{
community_enrollments: community_enrollments,
public_enrollments: public_enrollments,
package_enrollments: package_enrollments,
learninglab_enrollments: learninglab_enrollments
}
end
def get_community_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.is_archived == false,
where: not is_nil(cp.community_id),
where: cp.is_learning_lab == false,
# distinct: true,
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
def get_public_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.is_archived == false,
where: is_nil(cp.community_id),
where: is_nil(cp.package_period_id),
where: cp.is_learning_lab == false,
# distinct: true,
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
def get_learninglab_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.is_archived == false,
where: is_nil(cp.community_id),
where: cp.is_learning_lab == true,
# distinct: true,
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
def get_package_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.is_archived == false,
where: not is_nil(e.package_period_id),
where: not is_nil(e.course_period_id),
where: is_nil(cp.community_id),
where: cp.is_learning_lab == false,
where: not is_nil(cp.package_period_id),
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
def get_notcompleted_community_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: is_nil(e.passed_on) and e.course_completed == false,
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.is_archived == false,
where: not is_nil(cp.community_id),
where: cp.is_learning_lab == false,
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
def get_notcompleted_public_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: is_nil(e.passed_on) and e.course_completed == false,
where: is_nil(cp.community_id),
where: is_nil(cp.package_period_id),
where: cp.is_learning_lab == false,
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.is_archived == false,
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
def get_notarchived_completed_enrollments_for(user_struct) do
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: e.is_archived == false,
where: e.course_completed == true,
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
order_by: [asc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, :buddyship, :buddy],
limit: 5
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
end
@doc """
Builds a map containing all enrollments archive for a given user
Keys are: :community_enrollments, :public_enrollments, :learninglab_enrollments
"""
def get_archive_enrollments_for(user_struct) do
%{}
|> get_community_enrollment_archive_for(user_struct)
|> get_public_enrollment_archive_for(user_struct)
|> get_package_enrollment_archive_for(user_struct)
|> get_learninglab_enrollment_archive_for(user_struct)
end
def get_community_enrollment_archive_for(map, user_struct) do
enrollments =
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.updated_at <= ^Timex.shift(DateTime.utc_now(), days: -29),
where: cp.is_learning_lab == false,
where: not is_nil(cp.community_id),
where: cp.ends_on < ^Timex.now(),
order_by: [desc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: :scores,
preload: :buddyship,
preload: :buddy
)
)
|> Enum.map(fn e -> Ami.Enrollment.progress_for(e) end)
|> Enum.map(fn e -> Ami.Enrollment.mark_as_archived(e) end)
Map.put_new(map, :community_enrollments, enrollments)
end
def get_public_enrollment_archive_for(map, user_struct) do
if user_struct.status in [0, 1] do
enrollments =
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.updated_at <= ^Timex.shift(DateTime.utc_now(), days: -29),
where: cp.is_learning_lab == false,
where: is_nil(cp.community_id),
where: cp.ends_on < ^Timex.now(),
order_by: [desc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, buddyship: :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
|> Enum.map(fn e -> Enrollment.mark_as_archived(e) end)
Map.put_new(map, :public_enrollments, enrollments)
else
Map.put_new(map, :public_enrollments, [])
end
end
def get_learninglab_enrollment_archive_for(map, user_struct) do
enrollments =
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.status in [0, 1],
where: e.updated_at <= ^Timex.shift(DateTime.utc_now(), days: -29),
where: cp.is_learning_lab == true,
where: is_nil(cp.community_id),
where: cp.ends_on < ^Timex.now(),
order_by: [desc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: [:scores, buddyship: :buddy]
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
|> Enum.map(fn e -> Enrollment.mark_as_archived(e) end)
Map.put_new(map, :learninglab_enrollments, enrollments)
end
def get_package_enrollment_archive_for(map, user_struct) do
enrollments =
Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
join: c in assoc(cp, :course),
where: e.user_id == ^user_struct.id,
where: e.active == true,
where: e.updated_at <= ^Timex.shift(DateTime.utc_now(), days: -29),
where: e.status in [0, 1],
where: not is_nil(e.package_period_id),
where: not is_nil(e.course_period_id),
where: is_nil(cp.community_id),
where: cp.is_learning_lab == false,
where: not is_nil(cp.package_period_id),
where: cp.ends_on < ^Timex.now(),
order_by: [desc: cp.ends_on, asc: c.title],
preload: [
course_period: {cp, course: {c, [:thumb_image_attachments, instructors: :profile]}}
],
preload: :scores,
preload: :buddyship,
preload: :buddy
)
)
|> Enum.map(fn e -> Enrollment.progress_for(e) end)
|> Enum.map(fn e -> Enrollment.mark_as_archived(e) end)
Map.put_new(map, :package_enrollments, enrollments)
end
@doc """
Progress overview for a course : calculate progress percentage based on how many unit exams has been done along with the final exam. Provide a keyword list of all graded exams status for helping in setting stars status on progress bar.
{:pcentage, :completed_unit_test, :passed_final_exam, :accepted_final_exam, :exam_status, :unit_test_count}
"""
def calculate_progress(enrollment) do
%{pcentage: 0}
|> get_unit_exam_count(enrollment)
|> done_unit_test_count(enrollment)
|> has_passed_final_exam?(enrollment)
|> final_assignment_reviewed?(enrollment)
|> finished_course?(enrollment)
|> unit_tests_attempt_status(enrollment)
|> calculate_progress_percentage
end
def progress_for(enrollment) do
# empty_progress = :erlang.term_to_binary(%{})
progress =
case enrollment.progress do
<<131, 116, 0, 0, 0, 0>> ->
{:ok, enrollment} = update_progress(enrollment)
:erlang.binary_to_term(enrollment.progress)
nil ->
{:ok, enrollment} = update_progress(enrollment)
:erlang.binary_to_term(enrollment.progress)
_ ->
{:ok, enrollment} = update_progress(enrollment)
:erlang.binary_to_term(enrollment.progress)
end
%Enrollment{enrollment | progress: progress}
end
def mark_as_archived(enrollment) do
{:ok, enrollment} =
case enrollment.is_archived do
false ->
Enrollment.update(enrollment, %{is_archived: true})
true ->
{:ok, enrollment}
end
enrollment
end
def update_is_archived(enrollment) do
Ecto.Changeset.change(enrollment, %{is_archived: true}) |> Repo.update()
end
@doc """
Query responsible for fetching a given course unit tests
Bindings are : u - Unit / l - Lesson / q - Quiz
"""
def fetch_unit_tests_query(course) do
from(
u in Unit,
where: u.course_id == ^course.id,
join: l in assoc(u, :lessons),
join: q in assoc(l, :quiz),
where: q.is_graded == true and q.is_final == false
)
end
@doc """
Query responsible for fetching a given course final unit
"""
def fetch_final_unit_query(course) do
from(
u in Unit,
where: u.course_id == ^course.id,
join: l in assoc(u, :lessons),
join: q in assoc(l, :quiz),
where: q.is_final == true
)
end
@doc """
Count the enrollment's course unit tests and add it to the progress map under the key :unit_test_count
"""
def get_unit_exam_count(progress_map, enrollment) do
query = fetch_unit_tests_query(enrollment.course_period.course)
count = Repo.one(from([u, l, q] in query, select: count(q.id)))
Map.put_new(progress_map, :unit_test_count, count)
end
@doc """
Count the enrollment's done unit tests and add it to the progress map under the key :completed_unit_test
"""
def done_unit_test_count(progress_map, enrollment) do
query = fetch_unit_tests_query(enrollment.course_period.course)
count =
Repo.all(from([u, l, q] in query, select: q.id))
|> Enum.filter(fn quiz_id ->
Enum.find(enrollment.scores, fn s -> s.quiz_id == quiz_id end)
end)
|> length
Map.put_new(progress_map, :completed_unit_test, count)
end
@doc """
Query responsible for fetching course final quiz
Bindings are : q - Quiz / l - Lesson / u - Unit
"""
def fetch_final_exam_query(course) do
from(
q in Quiz,
join: l in assoc(q, :lesson),
join: ul in assoc(l, :unit_lessons),
join: u in assoc(ul, :unit),
where: u.course_id == ^course.id,
where: q.is_final == true
)
end
def fetch_final_exam_query_with_grade_value(course) do
from(
q in Quiz,
join: l in assoc(q, :lesson),
join: ul in assoc(l, :unit_lessons),
join: u in assoc(ul, :unit),
where: u.course_id == ^course.id,
where: q.is_final == true,
select: {q, u.grade_value}
)
end
@doc """
Query fetching the preassessment quiz of a given course
Bindings are : [q,l,u]
"""
def preassessment_quiz_query(course) do
from(
q in Quiz,
where: q.id == ^course.pre_assessment_quiz_id,
where: q.is_pre == true
)
end
@doc """
Query responsible for fetching the final quiz score for a given enrollment
Bindings are : s - Score
"""
def fetch_finalexam_score_query(final_exam_id, enrollment) do
from(
s in Score,
where: s.quiz_id == ^final_exam_id,
where: s.course_period_id == ^enrollment.course_period_id,
where: s.enrollment_id == ^enrollment.id,
where: s.user_id == ^enrollment.user_id
)
end
@doc """
Check if user has taken the course final exam or not, and add the check result to the progress_map under key :passed_final_exam
"""
def has_passed_final_exam?(progress_map, enrollment) do
query = fetch_final_exam_query(enrollment.course_period.course)
final_exam_id = Repo.one(from([q, l, u] in query, select: q.id))
case final_exam_id do
nil ->
Map.put_new(progress_map, :passed_final_exam, false)
_ ->
did_finalexam =
Enum.any?(enrollment.scores, fn score -> score.quiz_id == final_exam_id end)
if did_finalexam do
Map.put_new(progress_map, :passed_final_exam, true)
else
Map.put_new(progress_map, :passed_final_exam, false)
end
end
end
def final_assignment_reviewed?(progress_map, enrollment) do
Map.put_new(
progress_map,
:assignment_reviewed,
enrollment.assignment_status == "accepted" || enrollment.assignment_status == "rejected"
)
end
@doc """
Check wether or not the user succeeded this enrollment final exam and add the result to the progress map under key :accepted_final_exam
"""
def finished_course?(progress_map, enrollment) do
Map.put_new(progress_map, :completed_course, Enrollment.course_is_completed?(enrollment))
end
@doc """
Calculate the enrollment progress percentage following this rule/formula
1 - calculate the unit test pcentage value :
50.0 / unit tests count
50.0 if unit test count = 0
2 - add 25 if user has passed final exam
3 - add 25 if user has succeeded final exam
Add the calculated percentage to the progress map under key :pcentage
"""
def calculate_progress_percentage(progress_map) do
nb_of_unit_exams =
cond do
progress_map[:unit_test_count] == 0 -> 1
progress_map[:unit_test_count] > 0 -> progress_map[:unit_test_count]
end
utest_pcent_value = 50.0 / nb_of_unit_exams
progress_map = %{
progress_map
| pcentage: progress_map[:completed_unit_test] * utest_pcent_value
}
progress_map =
if progress_map.passed_final_exam,
do: Map.update!(progress_map, :pcentage, &(&1 + 25)),
else: progress_map
if progress_map.completed_course,
do: Map.update!(progress_map, :pcentage, &(&1 + 25)),
else: progress_map
end
@doc """
Return an ordered by unit_id list tuples in the form [{ unit_id, 1(true)/0(false)}, idx }, {{},idx2}, …] where 1/0 stands for wether the user has done the unit test
"""
def unit_tests_attempt_status(progress_map, enrollment) do
keyword_list = []
# get the unit_exam ids in descending order bc we'll prepend element in final keywords list
unit_test_query = fetch_unit_tests_query(enrollment.course_period.course)
unit_ids = Repo.all(from([u, l, q] in unit_test_query, order_by: [desc: u.id], select: u.id))
final_exam_query = fetch_final_exam_query(enrollment.course_period.course)
final_quiz = Repo.one(from([q, l, u] in final_exam_query))
# NOTE : this is ugly !!! We couldn't create a course with a final exam defined
# NOTE from Sadan: This is the old implementation which is causing compilation issue in docker.
# Please look at the fix below
# if !is_nil(final_quiz) do
# final_quiz_id = final_quiz.id
# final_quiz_title = final_quiz.title
# final_quiz_lesson_id = final_quiz.lesson_id
# final_quiz_score = fetch_score_for(final_quiz.id, enrollment)
# else
# final_quiz_id = nil
# final_quiz_title = ""
# final_quiz_lesson_id = nil
# final_quiz_score = nil
# end
{final_quiz_id, final_quiz_title, final_quiz_lesson_id, final_quiz_score} =
if !is_nil(final_quiz) do
{final_quiz.id, final_quiz.title, final_quiz.lesson_id,
fetch_score_for(final_quiz.id, enrollment)}
else
{nil, "", nil, nil}
end
conclusion_unit_id = Repo.one(from([q, l, u] in final_exam_query, select: u.id))
# associate them with their corresponding score presence
# starting with final assignment status
keyword_list =
case progress_map.assignment_reviewed do
true -> [{"final assignment", 1, nil, "Final assignment", "approved"} | keyword_list]
false -> [{"final assignment", 0, nil, "Final assignment", "pending"} | keyword_list]
end
# then with course conlusion unit test
keyword_list =
case progress_map.passed_final_exam do
true ->
[
{
conclusion_unit_id,
1,
final_quiz_id,
final_quiz_title,
final_quiz_lesson_id,
final_quiz_score.value
}
| keyword_list
]
false ->
[
{
conclusion_unit_id,
0,
final_quiz_id,
final_quiz_title,
final_quiz_lesson_id,
"not taken yet"
}
| keyword_list
]
end
# then the other unit tests
tuples_list =
Stream.map(unit_ids, fn unit_id ->
# TODO : check admin/moderator does not create multiple graded quizzes for a same Unit ?
get_unit_graded_quiz(unit_id)
|> create_base_tuple(unit_id)
|> add_lesson_title_and_id
|> add_score_value(enrollment)
|> add_took_exam_flag
end)
|> Enum.take(length(unit_ids))
|> prepend_to_list(keyword_list)
|> Enum.with_index()
Map.put_new(progress_map, :exam_status, tuples_list)
end
def get_unit_graded_quiz(unit_id) do
query =
from(
q in Quiz,
join: l in assoc(q, :lesson),
join: ul in assoc(l, :unit_lessons),
join: u in assoc(ul, :unit),
where: u.id == ^unit_id,
where: q.is_graded == true and q.is_final == false
)
Repo.one(from([q, l, u] in query, preload: [lesson: l]))
end
@doc """
Helper functions pipeline building enrollment quizzes status stored in enrollment.progress
"""
def create_base_tuple(quiz, unit_id), do: {unit_id, quiz}
def add_lesson_title_and_id({unit_id, quiz}),
do: {unit_id, quiz.id, quiz.lesson.title, quiz.lesson_id}
def add_score_value({unit_id, quiz_id, lesson_title, lesson_id}, enrollment) do
score = fetch_score_for(quiz_id, enrollment)
case score do
nil -> {unit_id, quiz_id, lesson_title, lesson_id, nil}
_ -> {unit_id, quiz_id, lesson_title, lesson_id, score.value}
end
end
def add_took_exam_flag({unit_id, quiz_id, lesson_title, lesson_id, score_value}) do
case score_value do
nil -> {unit_id, 0, quiz_id, lesson_title, lesson_id, "not taken yet"}
_ -> {unit_id, 1, quiz_id, lesson_title, lesson_id, score_value}
end
end
def fetch_score_for(quiz_id, enrollment) do
if quiz_id do
query =
from(
s in Score,
where: s.quiz_id == ^quiz_id,
where: s.enrollment_id == ^enrollment.id and s.user_id == ^enrollment.user_id,
where: s.course_period_id == ^enrollment.course_period_id,
select: [:id, :quiz_id, :enrollment_id, :user_id, :course_period_id, :value],
order_by: [asc: :value]
)
try do
Repo.one(query)
rescue
Ecto.MultipleResultsError ->
Repo.all(query) |> List.last()
end
end
end
def prepend_to_list([], list), do: list
def prepend_to_list([head | tail], list) do
prepend_to_list(tail, [head | list])
end
def update(enrollment, params \\ %{}) do
Ecto.Changeset.change(enrollment, params) |> Repo.update()
end
def potential_buddy_ids_for(course_period, enrollment) do
# get users ids with no buddyship for this course period
Repo.all(
from(
e in Enrollment,
where: e.course_period_id == ^course_period.id,
where: e.user_id != ^enrollment.user_id,
where: is_nil(e.buddyship_id),
select: e.user_id
)
)
|> Enum.sort()
end
def potential_buddy_ids_for(users_list, :additional, course_period, enrollment) do
if length(users_list) > 0 do
users_list
else
potential_buddy_ids_for(:additional, course_period, enrollment)
end
end
def potential_buddy_ids_for(:additional, course_period, enrollment) do
# scope :with_count_spot_to_become_buddy, ->(course_period, count) { joins("INNER JOIN buddyships on buddyships.buddy_id = users.id AND buddyships.course_period_id= #{course_period.id}").group('users.id').having("count(buddyships.id) = ?", count)}
# query responsible for getting user ids who strictly have 1 buddyships for this course period
from(
u in User,
join: b in assoc(u, :assigned_buddyships),
where: b.course_period_id == ^course_period.id,
where: b.buddy_id != ^enrollment.user_id and b.user_id != ^enrollment.user_id,
group_by: u.id,
having: count(b.id) == 1,
select: {u.id, count(b.id)}
)
|> Repo.all()
|> Enum.map(fn {user_id, _} -> user_id end)
end
def potential_buddy_ids_for(course_period, enrollment, excluded_userid) do
query =
from(
b in Buddyship,
where: b.course_period_id == ^course_period.id,
where: b.buddy_id != ^enrollment.user_id and b.user_id != ^enrollment.user_id,
where: b.buddy_id != ^excluded_userid and b.user_id != ^excluded_userid,
group_by: b.buddy_id,
having: count(b.id) == 1,
select: {b.buddy_id, count(b.id)}
)
virgin_user_ids =
Repo.all(
from(
e in Enrollment,
where: e.course_period_id == ^course_period.id,
where: e.user_id != ^enrollment.user_id,
where: e.user_id != ^excluded_userid,
where: is_nil(e.buddyship_id),
select: e.user_id
)
)
Repo.all(query)
|> Enum.map(fn {user_id, _} -> user_id end)
|> Enum.concat(virgin_user_ids)
|> Enum.sort()
end
def type_of(%Enrollment{} = enrollment) do
cond do
not is_nil(enrollment.course_period_id) && not is_nil(enrollment.course_period.community_id) ->
"community"
not is_nil(enrollment.course_period_id) && enrollment.course_period.is_learning_lab == true ->
"learning_lab"
not is_nil(enrollment.course_period_id) && is_nil(enrollment.course_period.community_id) &&
enrollment.course_period.is_learning_lab == false && is_nil(enrollment.package_period_id) ->
"public"
is_nil(enrollment.course_period_id) && not is_nil(enrollment.package_period_id) ->
"package"
not is_nil(enrollment.package_period_id) && not is_nil(enrollment.course_period_id) ->
"package"
end
end
def course_passed?(%Enrollment{} = enrollment) do
enrollment = Repo.preload(enrollment, [:scores, course_period: :course], force: true)
calculate_global_result(enrollment) >= enrollment.course_period.course.acceptance_score &&
course_is_completed?(enrollment) && assignment_accepted?(enrollment)
end
@doc """
Calculate an enrollment's final grade, according to following rules :
- 40% for accepted assignment
- 40% of final exam score
- 20% of unit test scores average
"""
def calculate_global_result(%Enrollment{} = enrollment) do
assignment_part =
if assignment_accepted?(enrollment) do
assignment_grade_value =
from(
l in Lesson,
join: u in assoc(l, :units),
join: la in assoc(l, :lesson_activity),
where: la.is_final_assignment == true,
where: u.course_id == ^enrollment.course_period.course_id,
distinct: true,
select: la.grade_value
)
|> Repo.one()
case assignment_grade_value do
n when is_integer(n) -> n
nil -> 40
_ -> 40
end
else
0
end
final_exam_part =
case final_exam_result(enrollment) do
{value, grade_value} when grade_value != nil ->
value * (grade_value / 100)
{value, grade_value} when is_nil(grade_value) ->
value * 0.4
end
test_part =
length(Course.unit_exams(enrollment.course_period.course))
|> case do
n when n > 0 ->
query =
from(
s in Score,
join: q in assoc(s, :quiz),
join: l in assoc(q, :lesson),
join: u in assoc(l, :units),
where: s.enrollment_id == ^enrollment.id,
where: s.course_period_id == ^enrollment.course_period_id,
where: s.user_id == ^enrollment.user_id,
where: q.is_graded == true,
where: q.is_final == false,
where: u.course_id == ^enrollment.course_period.course_id,
select: {s.value, u.grade_value}
)
unit_tests_total =
Repo.all(from(s in query))
|> Enum.map(fn {s, g} ->
case g do
# apply old grade value
nil -> {s, 20}
# apply recorded grade value
_ -> {s, g}
end
end)
|> Enum.reduce(0, fn {s, g}, acc -> s * (g / 100) + acc end)
unit_tests_total / n
_ ->
0
end
(test_part + final_exam_part + assignment_part) |> Float.round(2)
# tests_avg =
# length(Course.unit_exams(enrollment.course_period.course))
# |> case do
# n when n > 0 -> unit_tests_total_score(enrollment) / n
# _ -> 0
# end
# (tests_avg * 0.2 + final_exam_part + assignment_part) |> Float.round(2)
end
@doc """
Update enrollment's course flags (started, completed, passed_on) based enrollment scores and assignment status
"""
def update_course_flags(%Enrollment{} = enrollment) do
{:ok, enrollment} =
if !enrollment.course_started do
__MODULE__.update(enrollment, %{course_started: true})
else
{:ok, enrollment}
end
{:ok, enrollment} =
if __MODULE__.course_is_completed?(enrollment) do
__MODULE__.update(enrollment, %{course_completed: true})
else
{:ok, enrollment}
end
{:ok, enrollment} =
if __MODULE__.course_passed?(enrollment) do
__MODULE__.update(enrollment, %{passed_on: Timex.now()})
else
{:ok, enrollment}
end
end
defp final_exam_result(enrollment) do
final_exam_query = fetch_final_exam_query_with_grade_value(enrollment.course_period.course)
{final_quiz, gv} = Repo.one(from([q, l, u] in final_exam_query))
value =
Enum.find(enrollment.scores, %Score{value: 0}, fn s -> s.quiz_id == final_quiz.id end).value
{value, gv}
end
defp unit_tests_total_score(enrollment) do
query = test_results_query(enrollment)
Repo.all(from(s in query, select: s.value))
|> Enum.reduce(0, fn s, acc -> s + acc end)
end
defp assignment_accepted?(enrollment) do
enrollment.assignment_status == "accepted"
end
defp assignment_rejected?(enrollment) do
enrollment.assignment_status == "rejected"
end
def test_results_query(enrollment) do
from(
s in Score,
join: q in assoc(s, :quiz),
where: s.enrollment_id == ^enrollment.id,
where: s.course_period_id == ^enrollment.course_period_id,
where: s.user_id == ^enrollment.user_id,
where: q.is_graded == true,
where: q.is_final == false
)
end
def course_is_completed?(enrollment) do
graded_exam_ids =
Course.graded_exams(enrollment.course_period.course)
|> Enum.map(fn e -> e.id end)
graded_scores = Enum.filter(enrollment.scores, fn s -> s.quiz_id in graded_exam_ids end)
(assignment_accepted?(enrollment) || assignment_rejected?(enrollment)) &&
length(graded_exam_ids) == length(graded_scores)
end
def delete_associated_datas_of(enrollment) do
from(
ua in UserAnswer,
where: ua.user_id == ^enrollment.user_id,
where: ua.course_period_id == ^enrollment.course_period_id
)
|> Repo.delete_all()
from(
s in Score,
where: s.enrollment_id == ^enrollment.id,
where: s.user_id == ^enrollment.user_id
)
|> Repo.delete_all()
unless is_nil(enrollment.buddyship_id) do
Repo.get(Buddyship, enrollment.buddyship_id) |> Repo.delete()
end
from(
r in Resource,
where: r.resourceable_type == "Enrollment",
where: r.resourceable_id == ^enrollment.id
)
|> Repo.delete_all()
end
def into_learning_lab?(user_struct) do
enrollments =
Ami.Repo.all(
from(
e in Ami.Enrollment,
join: cp in assoc(e, :course_period),
where: e.user_id == ^user_struct.id,
where: cp.is_learning_lab == true,
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today()
)
)
length(enrollments) > 0
end
def into_package?(user_struct) do
Ami.Repo.one(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
where: e.user_id == ^user_struct.id,
where: not is_nil(e.package_period_id),
where: not is_nil(e.course_period_id),
where: is_nil(cp.community_id),
where: cp.is_learning_lab == false,
where: not is_nil(cp.package_period_id),
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today(),
select: count(e.id)
)
) > 0
end
def into_community_cp?(user_struct) do
enrollments =
Ami.Repo.all(
from(
e in __MODULE__,
join: cp in assoc(e, :course_period),
where: e.user_id == ^user_struct.id,
where: not is_nil(cp.community_id),
where: cp.starts_on <= ^Date.utc_today() and cp.ends_on >= ^Date.utc_today()
)
)
length(enrollments) > 0
end
def days_from_ending?(enrollment, countdown) do
enrollment = Ami.Repo.preload(enrollment, :course_period)
interval =
Interval.new(
from: Date.utc_today(),
until: enrollment.course_period.ends_on
)
|> Interval.duration(:days)
interval == countdown
end
defp other_cp_students_query(course_period, user_id) do
from(
u in Ami.User,
join: e in assoc(u, :enrollments),
left_join: pr in assoc(u, :profile),
where: u.id != ^user_id,
where: e.course_period_id == ^course_period.id,
where: e.active == true,
preload: [profile: pr],
select: [:id, :email, :name, :uuid, profile: [:id, :first_name, :last_name, :image]]
)
end
def other_cp_students(course_period, user) do
other_cp_students_query(course_period, user.id) |> Repo.all()
end
def remaining_assessments(enrollment) do
course_id = enrollment.course_period.course_id
taken_quiz_ids = Enum.map(enrollment.scores, fn score -> score.quiz_id end)
Ami.Repo.one(
from(
q in Ami.Quiz,
join: l in assoc(q, :lesson),
join: u in assoc(l, :unit),
join: c in assoc(u, :course),
where: c.id == ^course_id,
where: q.is_graded == true or q.is_final == true,
where: not (q.id in ^taken_quiz_ids),
select: count(q.id)
)
)
end
end