Current section

Files

Jump to
ami lib ami course.ex
Raw

lib/ami/course.ex

defmodule Ami.Course do
use Ecto.Schema
use Arc.Ecto.Schema
import Ecto.{
Query,
Changeset
}
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
@timestamps_opts [type: :naive_datetime_usec]
field(:title, :string)
field(:description, :string)
field(:instructor, :string)
field(:image, :string)
# field(:tags, {:array, :string})
field(:created_at, :utc_datetime_usec)
field(:updated_at, :utc_datetime_usec)
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)
field(:marking_guide, Ami.MarkingGuideUploader.Type)
field(:sequential_tags, :string, virtual: true)
field(:level, :integer)
field(:migration_started, :boolean)
field(:migration_finished, :boolean)
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(:videos, Video, foreign_key: :videoable_id)
# 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 """
"""
def changeset(%Course{} = course, params \\ %{}) do
course
|> cast(params, [
:title,
:subtitle,
:description,
:course_admin_id,
:price,
:price_currency,
:free_course,
:certificate_price,
:access_price,
:on_air,
:marking_guide,
:total_hours,
:teaser_description,
:teaser_banner_text,
:components,
:why_take_course,
:is_featured,
:slug,
:acceptance_score,
:created_at,
:updated_at,
:sort,
:level,
:community_id
])
|> cast(
%{created_at: Timex.now(), updated_at: Timex.now()},
[:created_at, :updated_at]
)
|> parse_tags
|> cast_attachments(params, [:marking_guide])
|> cast_assoc(:units)
|> cast_assoc(:courses_resources)
|> validate_required([
:title,
:description,
:on_air,
:acceptance_score
])
end
def community_course_changeset(%Course{} = course, params \\ %{}) do
course
|> cast(params, [
:title,
:subtitle,
:description,
:course_admin_id,
:price,
:price_currency,
:free_course,
:certificate_price,
:access_price,
:on_air,
:marking_guide,
:total_hours,
:teaser_description,
:teaser_banner_text,
:components,
:why_take_course,
:is_featured,
:slug,
:acceptance_score,
:created_at,
:updated_at,
:sort,
:level,
:community_id
])
|> cast(
%{created_at: Timex.now(), updated_at: Timex.now()},
[:created_at, :updated_at]
)
|> parse_tags
|> cast_attachments(params, [:marking_guide])
|> cast_assoc(:units)
|> cast_assoc(:courses_resources)
|> validate_required([
:title,
:description,
:on_air,
:acceptance_score,
:community_id
])
end
def parse_tags(changeset) do
if is_nil(changeset.params["sequential_tags"]) do
changeset
else
stringify_tags = changeset.params["sequential_tags"]
tags =
String.split(stringify_tags, ", ", trim: true)
|> Enum.map(fn tag -> find_or_build_tag(tag) end)
put_change(changeset, :tags, tags)
end
end
def find_or_build_tag(tag_name) do
Tag.find_or_build_tag(tag_name)
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
def levels_map do
%{
0 => "Novice",
1 => "Advanced beginner",
2 => "Competent",
3 => "Proficient",
4 => "Expert"
}
end
def readable_level_for(course) do
levels_map[course.level]
end
def clone(%__MODULE__{} = course) do
course
|> clone_course_struct()
|> clone_thumb_image_attachments()
|> clone_teaser_image_attachments()
|> clone_affiliate_logo_attachments()
|> clone_tags()
|> clone_units()
|> clone_instructors()
|> clone_course_resources()
end
def clone_course_struct(course) do
clone_params =
Map.delete(course, :id)
|> Map.drop([
:tags,
:units,
:videos,
:lessons,
:quizzes,
:community,
:__meta__,
:resources,
:__struct__,
:instructors,
:thumb_images,
:course_admin,
:marking_guide,
:course_periods,
:courses_resources,
:pre_assessment_quiz,
:resource_attachments,
:thumb_image_attachments,
:teaser_image_attachments,
:affiliate_logo_attachments
])
|> Map.put(:title, "#{course.title} cloned")
clone_changeset = Course.changeset(%Course{}, clone_params)
case Repo.insert(clone_changeset) do
{:ok, cloned_course} ->
{course, cloned_course}
{:error, %Ecto.Changeset{} = clone_changeset} ->
raise "Cannot clone this course: #{course.title}"
end
end
def clone_thumb_image_attachments({course, cloned_course}) do
course = Repo.preload(course, [:thumb_image_attachments])
cloned_course = Repo.preload(cloned_course, [:thumb_image_attachments])
if length(course.thumb_image_attachments) > 0 do
Enum.each(course.thumb_image_attachments, fn attachment ->
params =
Map.delete(attachment, :id)
|> Map.put(:course_id, cloned_course.id)
ThumbImageAttachment.changeset(%ThumbImageAttachment{}, params)
|> Repo.insert()
end)
cloned_course = Repo.preload(cloned_course, [:thumb_image_attachments])
{course, cloned_course}
else
{course, cloned_course}
end
end
def clone_teaser_image_attachments({course, cloned_course}) do
course = Repo.preload(course, [:teaser_image_attachments])
cloned_course = Repo.preload(cloned_course, [:teaser_image_attachments])
if length(course.teaser_image_attachments) > 0 do
Enum.each(course.teaser_image_attachments, fn attachment ->
params =
Map.delete(attachment, :id)
|> Map.put(:course_id, cloned_course.id)
TeaserImageAttachment.changeset(%TeaserImageAttachment{}, params)
|> Repo.insert()
end)
cloned_course = Repo.preload(cloned_course, [:teaser_image_attachments])
{course, cloned_course}
else
{course, cloned_course}
end
end
def clone_affiliate_logo_attachments({course, cloned_course}) do
course = Repo.preload(course, [:affiliate_logo_attachments])
cloned_course = Repo.preload(cloned_course, [:affiliate_logo_attachments])
if length(course.affiliate_logo_attachments) > 0 do
Enum.each(course.affiliate_logo_attachments, fn attachment ->
params =
Map.delete(attachment, :id)
|> Map.put(:course_id, cloned_course.id)
AffiliateLogoAttachment.changeset(%AffiliateLogoAttachment{}, params)
|> Repo.insert()
end)
cloned_course = Repo.preload(cloned_course, [:affiliate_logo_attachments], force: true)
{course, cloned_course}
else
{course, cloned_course}
end
end
def clone_tags({course, cloned_course}) do
course = Repo.preload(course, [:tags])
cloned_course = Repo.preload(cloned_course, [:tags])
if length(course.tags) > 0 do
{:ok, cloned_course} =
Course.changeset(cloned_course)
|> put_change(:tags, course.tags)
|> Repo.update()
{course, cloned_course}
else
{course, cloned_course}
end
end
def clone_units({course, cloned_course}) do
course = Repo.preload(course, [units: :unit_lessons], force: true)
cloned_course = Repo.preload(cloned_course, units: :unit_lessons)
if length(course.units) > 0 do
Enum.each(course.units, fn unit ->
params =
Map.drop(unit, [:id, :__meta__, :__struct__])
|> Map.put(:title, "#{unit.title} cloned")
|> Map.put(:course_id, cloned_course.id)
{:ok, new_unit} =
Unit.changeset(%Unit{}, params)
|> Repo.insert()
Enum.each(unit.unit_lessons, fn ul ->
params =
Map.drop(ul, [:id, :__struct__, :__meta__])
|> Map.put(:unit_id, new_unit.id)
UnitLesson.changeset(%UnitLesson{}, params)
|> Repo.insert()
end)
end)
cloned_course = Repo.preload(cloned_course, [units: :unit_lessons], force: true)
{course, cloned_course}
else
{course, cloned_course}
end
end
def clone_instructors({course, cloned_course}) do
course = Repo.preload(course, [:instructors])
cloned_course = Repo.preload(cloned_course, [:instructors])
if length(course.instructors) > 0 do
{:ok, cloned_course} =
Course.changeset(cloned_course)
|> put_change(:instructors, course.instructors)
|> Repo.update()
cloned_course = Repo.preload(cloned_course, [:instructors], force: true)
{course, cloned_course}
else
{course, cloned_course}
end
end
def clone_course_resources({course, cloned_course}) do
course = Repo.preload(course, [:courses_resources], force: true)
cloned_course = Repo.preload(cloned_course, [:courses_resources])
if length(course.courses_resources) > 0 do
Enum.each(course.courses_resources, fn cr ->
params =
Map.drop(cr, [:id, :__struct__, :__meta__, :resource_attachment])
|> Map.put(:course_id, cloned_course.id)
|> IO.inspect()
{:ok, new_course_resource} =
CourseResource.changeset(%CourseResource{}, params)
|> Repo.insert()
end)
cloned_course = Repo.preload(cloned_course, [:courses_resources], force: true)
{course, cloned_course}
else
{course, cloned_course}
end
end
end