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 field(:type, :string) field(:enrollment_date, :utc_datetime) field(:created_at, :utc_datetime) field(:updated_at, :utc_datetime) field(:assignment_status, :string) field(:active, :boolean) field(:passed_on, :utc_datetime) field(:status, :integer) field(:course_started, :boolean) field(:course_completed, :boolean) field(:assignment_score, :integer) field(:assignment_sent_on, :utc_datetime) 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