Packages
ami
0.3.5
0.3.9
0.3.8
retired
0.3.7
retired
0.3.6
retired
0.3.5
retired
0.3.4
retired
0.3.3
retired
0.3.2
retired
0.3.1
retired
0.3.0
retired
0.2.9
retired
0.2.8
retired
0.2.7
retired
0.2.6
retired
0.2.5
retired
0.2.3
retired
0.2.2
retired
0.2.1
retired
0.2.0
retired
0.1.9
retired
0.1.8
retired
0.1.7
retired
0.1.6
retired
0.1.5
retired
0.1.4
retired
Package containing all AMI Models shared between micro-services.
Retired package: Security issue - retiring
Current section
Files
Jump to
Current section
Files
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