defmodule Ami.Course do use Ecto.Schema import Ecto.{ Query } alias Ami.{ Tag, User, Repo, Quiz, Unit, Score, Video, Course, Answer, Package, Resource, Community, UnitLesson, ThumbImage, TeaserImage, CoursePeriod, AffiliateLogo, CourseResource, ResourceAttachment, ThumbImageAttachment, TeaserImageAttachment, AffiliateLogoAttachment } schema "courses" do field(:title, :string) field(:description, :string) field(:instructor, :string) field(:image, :string) # field(:tags, {:array, :string}) field(:created_at, :utc_datetime) field(:updated_at, :utc_datetime) field(:sort, :integer) field(:why_take_course, :string) field(:components, :string) field(:total_hours, :integer) field(:inactive_teaser_link, :string) field(:subtitle, :string) field(:acceptance_score, :integer) field(:free_course, :boolean) field(:on_air, :boolean) field(:teaser_description, :string) field(:teaser_banner_text, :string) field(:price, :integer) field(:certificate_price, :boolean) field(:access_price, :boolean) field(:price_currency, :string) field(:slug, :string) field(:is_featured, :boolean) field(:marking_guide, :string) belongs_to(:course_admin, User, foreign_key: :course_admin_id) # belongs_to(:quiz, Quiz) belongs_to(:pre_assessment_quiz, Quiz) # belongs_to(:final_exam, Quiz) belongs_to(:community, Community) many_to_many(:instructors, User, join_through: "courses_users", join_keys: [course_id: :id, user_id: :id], on_delete: :delete_all ) has_many(:course_periods, CoursePeriod) has_many(:videos, Video, foreign_key: :videoable_id) has_many(:units, Unit) has_many(:lessons, through: [:units, :lessons]) has_many(:quizzes, through: [:lessons, :quiz]) # Also a polymorphic association # has_many(:resources, Resource, foreign_key: :resourceable_id) # has_many(:teaser_images, TeaserImage, foreign_key: :teaseable_id) has_many(:teaser_image_attachments, TeaserImageAttachment, on_delete: :delete_all, on_replace: :delete ) has_many(:thumb_image_attachments, ThumbImageAttachment, on_delete: :delete_all, on_replace: :delete ) has_many(:affiliate_logo_attachments, AffiliateLogoAttachment, on_delete: :delete_all, on_replace: :delete ) has_many(:courses_resources, CourseResource) has_many(:resource_attachments, through: [:courses_resources, :resource_attachment], on_delete: :delate_all, on_replace: :delete ) # has_many(:affiliate_logos, AffiliateLogo) many_to_many(:tags, Tag, join_through: "courses_tags", on_replace: :delete, on_delete: :delete_all ) has_many(:thumb_images, ThumbImage) # has_many(:affiliate_logos, AffiliateLogo) # has_many(:scores, Score) # has_many(:lessons, through: [:units, :lessons]) end @doc """ Because of better design patterns implemented by Ecto we cannot return lesson.videos to return correct videos. preload_videos implement a pattern which will return lesson.videos to return only videos with videoable_type == "Lesson" for the given lesson """ def preload_videos(struct), do: Video.preload_videos(struct, "Course") @doc """ Pattern to fetch resources as lesson.resources with resourceable_type == "Lesson" """ def preload_resources(struct), do: Resource.preload_resources(struct, "Course") @doc """ Fetch courses to display on the my_courses page tailored for given user If given user is business member (2), fetch: - courses with open course periods belonging to the communities the user is member of - courses with open course periods belonging to the communities the user is moderator of If given user is member (1), fetch above plus the followings: - 'on air' featured courses - 'on air' open public courses - 'on air' closed public courses If given user is non-member (0) fetch: - 'on air' public courses - 'on air' courses with course periods belonging to communities - 'on air' courses with course periods belonging to 'Universal' and 'Public' communities """ def catalog_for(user_struct) do case user_struct.status do 0 -> courses_list = fetch_courses_for_nonmember(user_struct) packages_list = Package.on_air() %{courses: courses_list, packages: packages_list} 1 -> courses_list = fetch_courses_for_member(user_struct) packages_list = Package.on_air() %{courses: courses_list, packages: packages_list} 2 -> %{courses: CoursePeriod.open_community_courses_for(user_struct), packages: []} end end def catalog_for(user_struct, type) do courses_list = case type do "community" -> CoursePeriod.open_community_courses_for(user_struct) |> add_universal_public_community_courses(user_struct) |> Enum.uniq_by(fn %CoursePeriod{} = cp -> cp.course_id end) "public" -> CoursePeriod.for_open_on_air_public_courses() "package" -> [] "learning_lab" -> CoursePeriod.for_learning_lab() end packages_list = case type do "community" -> [] "public" -> packages_list_for(user_struct) "package" -> packages_list_for(user_struct) "learning_lab" -> [] end %{courses: courses_list, packages: packages_list} end def catalog_of_open_public_cp() do Repo.all(from(c in __MODULE__, where: c.on_air == true)) |> Enum.map( &(CoursePeriod.fetch_current_public_course_periods_for(&1) |> List.first()) ) |> Enum.reject(&is_nil(&1)) end defp add_universal_public_community_courses(cp_list, user_struct) do if user_struct.status == 0 do Enum.concat(cp_list, CoursePeriod.for_open_onair_universalpublic_community_courses()) else cp_list end end defp packages_list_for(user_struct) do if user_struct.status in [0, 1], do: Package.on_air(), else: [] end defp fetch_courses_for_nonmember(user_struct) do CoursePeriod.for_open_on_air_public_courses() |> Enum.concat(CoursePeriod.open_community_courses_for(user_struct)) |> Enum.concat(CoursePeriod.for_open_onair_universalpublic_community_courses()) |> Enum.uniq_by(fn %CoursePeriod{} = cp -> cp.course_id end) end defp fetch_courses_for_member(user_struct) do CoursePeriod.for_on_air_featured_courses() |> Enum.concat(CoursePeriod.open_community_courses_for(user_struct)) |> Enum.concat(CoursePeriod.for_open_on_air_public_courses()) |> Enum.uniq_by(fn %CoursePeriod{} = cp -> cp.course_id end) end def topics_for(course_struct) do Repo.preload(course_struct, units: from(u in Unit, order_by: u.id)).units |> Enum.map(fn u -> u.title end) |> Enum.drop(-1) end def distinct_instructors(course) do course.instructors |> Enum.dedup_by(fn e -> e.id end) end def unit_exams(%Course{} = course) 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), join: c in assoc(u, :course), where: c.id == ^course.id, where: q.is_graded == true, where: q.is_final == false ) Repo.all(query) end def graded_exams(%Course{} = course) 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), join: c in assoc(u, :course), where: c.id == ^course.id, where: q.is_graded == true or q.is_final == true ) Repo.all(query) end def final_exam(course_id) do query = from( q in Quiz, where: q.is_final == true, join: l in assoc(q, :lesson), join: ul in assoc(l, :unit_lessons), join: u in assoc(ul, :unit), # join: c in assoc(u, :course), where: u.course_id == ^course_id ) Repo.one(query) end def pre_assessment_quiz(course) do Repo.preload(course, [:quizzes]).quizzes |> Enum.find(fn q -> q.is_pre end) |> Repo.preload(questions: :answers) end def marking_guide_url({id, marking_guide}) do case marking_guide do nil -> "" _ -> "https://ami-aws-s3.s3.amazonaws.com/uploads/#{marking_guide}" end end def has_intro_activity?(course) do intro_unit = List.first(course.units) lesson = List.first(intro_unit.lessons) |> Repo.preload([:lesson_activity]) !is_nil(lesson.lesson_activity) end def intro_activity_url(course) do intro_unit = List.first(course.units) lesson = List.first(intro_unit.lessons) |> Repo.preload([:lesson_activity_attachment]) ResourceAttachment.file_url( {lesson.lesson_activity_attachment.id, lesson.lesson_activity_attachment.file} ) end def unit_for_lesson(course, lesson) do query = from( u in Unit, join: ul in assoc(u, :unit_lessons), join: l in assoc(ul, :lesson), where: u.course_id == ^course.id, where: l.id == ^lesson.id, order_by: [asc: ul.position] ) Repo.one(query) |> Repo.preload(unit_lessons: {from(ul in UnitLesson, order_by: ul.position), [:lesson]}) |> Repo.preload(:lessons) end end