Skip to content

Commit

Permalink
Merge pull request #4071 from Simon-Initiative/stagehand-data-generator
Browse files Browse the repository at this point in the history
[FEATURE] Stagehand data generator
  • Loading branch information
darrensiegel authored Aug 8, 2023
2 parents dd400ee + 6fa66aa commit 92720a5
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 86 deletions.
60 changes: 60 additions & 0 deletions lib/oli/delivery/sections.ex
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,66 @@ defmodule Oli.Delivery.Sections do
change_section(Map.merge(section, %{open_and_free: true}), attrs)
end

@doc """
Returns the set of all students with :context_learner role in the given section.
"""
def fetch_students(section_slug) do
list_enrollments(section_slug)
|> Enum.filter(fn e ->
ContextRoles.contains_role?(e.context_roles, ContextRoles.get_role(:context_learner))
end)
|> Enum.map(fn e -> e.user end)
end

@doc """
Returns the set of all instructors with :context_instructor role in the given section.
"""
def fetch_instructors(section_slug) do
list_enrollments(section_slug)
|> Enum.filter(fn e ->
ContextRoles.contains_role?(e.context_roles, ContextRoles.get_role(:context_instructor))
end)
|> Enum.map(fn e -> e.user end)
end

@doc """
Returns all scored pages for the given section.
"""
def fetch_scored_pages(section_slug), do: fetch_all_pages(section_slug, true)

@doc """
Returns all unscored pages for the given section.
"""
def fetch_unscored_pages(section_slug), do: fetch_all_pages(section_slug, false)

def fetch_all_pages(section_slug, graded \\ nil) do
maybe_filter_by_graded =
case graded do
nil -> true
graded -> dynamic([_, _, _, _, rev], rev.graded == ^graded)
end

SectionResource
|> join(:inner, [sr], s in Section, on: sr.section_id == s.id)
|> join(:inner, [sr, s], spp in SectionsProjectsPublications,
on: spp.section_id == s.id and spp.project_id == sr.project_id
)
|> join(:inner, [sr, _, spp], pr in PublishedResource,
on: pr.publication_id == spp.publication_id and pr.resource_id == sr.resource_id
)
|> join(:inner, [sr, _, _, pr], rev in Revision, on: rev.id == pr.revision_id)
|> where(
[sr, s, _, _, rev],
s.slug == ^section_slug and
rev.deleted == false and
rev.resource_type_id == ^ResourceType.get_id_by_type("page")
)
|> where(^maybe_filter_by_graded)
|> order_by([_, _, _, _, rev], asc: rev.resource_id)
|> select([_, _, _, _, rev], rev)
|> Repo.all()
end

# Creates a 'hierarchy definition' strictly from a a project and the recursive
# definition of containers starting with the root revision container. This hierarchy
# definition is a map of resource ids to a list of the child resource ids, effectively
Expand Down
54 changes: 3 additions & 51 deletions lib/oli/grading.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,15 @@ defmodule Oli.Grading do

alias Oli.Publishing.DeliveryResolver
alias Oli.Delivery.Sections
alias Oli.Delivery.Sections.{SectionResource, Section}
alias Oli.Delivery.Sections.{Section}
alias Oli.Delivery.Attempts.Core, as: Attempts
alias Oli.Delivery.Attempts.Core.ResourceAccess
alias Oli.Grading.GradebookRow
alias Oli.Grading.GradebookScore
alias Oli.Activities.Realizer.Selection
alias Lti_1p3.Tool.ContextRoles
alias Lti_1p3.Tool.Services.AGS
alias Lti_1p3.Tool.Services.AGS.Score
alias Oli.Resources.Revision
alias Oli.Publishing.PublishedResource
alias Oli.Resources.ResourceType

alias Oli.Repo
alias Oli.Delivery.Sections.SectionsProjectsPublications
alias OliWeb.Common.Utils

@doc """
Expand Down Expand Up @@ -170,10 +164,10 @@ defmodule Oli.Grading do
"""
def generate_gradebook_for_section(%Section{} = section) do
# get publication page resources, filtered by graded: true
graded_pages = fetch_graded_pages(section.slug)
graded_pages = Sections.fetch_scored_pages(section.slug)

# get students enrolled in the section, filter by role: student
students = fetch_students(section.slug)
students = Sections.fetch_students(section.slug)

# create a map of all resource accesses, keyed off resource id
resource_accesses = fetch_resource_accesses(section.id)
Expand Down Expand Up @@ -274,22 +268,6 @@ defmodule Oli.Grading do
defp ensure_valid_number(value) when is_float(value), do: value
defp ensure_valid_number(_), do: 1.0

def fetch_students(section_slug) do
Sections.list_enrollments(section_slug)
|> Enum.filter(fn e ->
ContextRoles.contains_role?(e.context_roles, ContextRoles.get_role(:context_learner))
end)
|> Enum.map(fn e -> e.user end)
end

def fetch_instructors(section_slug) do
Sections.list_enrollments(section_slug)
|> Enum.filter(fn e ->
ContextRoles.contains_role?(e.context_roles, ContextRoles.get_role(:context_instructor))
end)
|> Enum.map(fn e -> e.user end)
end

def fetch_resource_accesses(section_id) do
Attempts.get_graded_resource_access_for_context(section_id)
|> Enum.reduce(%{}, fn resource_access, acc ->
Expand All @@ -310,30 +288,4 @@ defmodule Oli.Grading do
end
end)
end

def fetch_graded_pages(section_slug) do
SectionResource
|> join(:inner, [sr], s in Section, on: sr.section_id == s.id)
|> join(:inner, [sr, s], spp in SectionsProjectsPublications,
on: spp.section_id == s.id and spp.project_id == sr.project_id
)
|> join(:inner, [sr, _, spp], pr in PublishedResource,
on: pr.publication_id == spp.publication_id and pr.resource_id == sr.resource_id
)
|> join(:inner, [sr, _, _, pr], rev in Revision, on: rev.id == pr.revision_id)
|> where(
[sr, s, _, _, rev],
s.slug == ^section_slug and
rev.deleted == false and
rev.graded == true and
rev.resource_type_id == ^ResourceType.get_id_by_type("page")
)
|> order_by([_, _, _, _, rev], asc: rev.resource_id)
|> select([_, _, _, _, rev], rev)
|> Repo.all()
end

def fetch_reachable_graded_pages(section_slug) do
fetch_graded_pages(section_slug)
end
end
30 changes: 13 additions & 17 deletions lib/oli/utils/seeder/attempt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ defmodule Oli.Utils.Seeder.Attempt do
alias Oli.Delivery.Attempts.ActivityLifecycle.Evaluate
alias Oli.Delivery.Attempts.PageLifecycle.AttemptState
alias Oli.Delivery.Attempts.ActivityLifecycle
alias Oli.Delivery.Page.PageContext

def visit_unscored_page(
def visit_page(
seeds,
page_revision,
section,
Expand All @@ -18,25 +19,19 @@ defmodule Oli.Utils.Seeder.Attempt do
[page_revision, section, user, datashop_session_id] =
unpack(seeds, [page_revision, section, user, datashop_session_id])

effective_settings = %Oli.Delivery.Settings.Combined{}
page_context =
PageContext.create_for_visit(section, page_revision.slug, user, datashop_session_id)

Core.track_access(page_revision.resource_id, section.id, user.id)

{:ok,
{:in_progress,
%AttemptState{resource_attempt: resource_attempt, attempt_hierarchy: attempt_hierarchy}}} =
PageLifecycle.visit(
page_revision,
section.slug,
datashop_session_id,
user,
effective_settings,
&Oli.Delivery.ActivityProvider.provide/6
)
resource_attempt =
case page_context.resource_attempts do
[resource_attempt | _] -> resource_attempt
_ -> nil
end

seeds
|> tag(tags[:resource_attempt_tag], resource_attempt)
|> tag(tags[:attempt_hierarchy_tag], attempt_hierarchy)
|> tag(tags[:attempt_hierarchy_tag], page_context.latest_attempts)
|> tag(tags[:page_context_tag], page_context)
end

def start_scored_assessment(
Expand All @@ -52,7 +47,8 @@ defmodule Oli.Utils.Seeder.Attempt do

Core.track_access(page_revision.resource_id, section.id, user.id)

effective_settings = Oli.Delivery.Settings.get_combined_settings(page_revision, section.id, user.id)
effective_settings =
Oli.Delivery.Settings.get_combined_settings(page_revision, section.id, user.id)

{:ok, %AttemptState{resource_attempt: resource_attempt, attempt_hierarchy: attempt_hierarchy}} =
PageLifecycle.start(
Expand Down
6 changes: 4 additions & 2 deletions lib/oli/utils/seeder/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ defmodule Oli.Utils.Seeder.Project do
},
graded: false
},
revision_tag: unscored_page1_tag
revision_tag: unscored_page1_tag,
container_revision_tag: unit1_tag
)
|> Seeder.Project.create_page(
author,
Expand All @@ -159,7 +160,8 @@ defmodule Oli.Utils.Seeder.Project do
},
graded: true
},
revision_tag: scored_page2_tag
revision_tag: scored_page2_tag,
container_revision_tag: unit1_tag
)

seeds
Expand Down
12 changes: 6 additions & 6 deletions lib/oli/utils/seeder/section.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,19 @@ defmodule Oli.Utils.Seeder.Section do
|> tag(tags[:section_tag], section)
end

def create_and_enroll_learner(seeds, section, user_attrs, tags \\ []) do
user_tag = tags[:user_tag] || random_tag()
def create_and_enroll_learner(seeds, section, user_attrs \\ %{}, tags \\ []) do
user_tag = tags[:user_tag] || random_tag("user")

seeds
|> create_user(user_attrs, tags)
|> create_user(user_attrs, Keyword.put(tags, :user_tag, user_tag))
|> enroll_as_learner(section, ref(user_tag))
end

def create_and_enroll_instructor(seeds, section, user_attrs, tags \\ []) do
user_tag = tags[:user_tag] || random_tag()
def create_and_enroll_instructor(seeds, section, user_attrs \\ %{}, tags \\ []) do
user_tag = tags[:user_tag] || random_tag("user")

seeds
|> create_user(user_attrs, tags)
|> create_user(user_attrs, Keyword.put(tags, :user_tag, user_tag))
|> enroll_as_instructor(section, ref(user_tag))
end

Expand Down
2 changes: 2 additions & 0 deletions lib/oli/utils/seeder/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ defmodule Oli.Utils.Seeder.Utils do
def ref(tag), do: %SeedRef{tag: tag}

def random_tag(), do: uuid()
def random_tag(label) when is_atom(label), do: random_tag(to_string(label))
def random_tag(label), do: "#{label}_#{uuid()}"

# do not add values for which tags are nil
def tag(seeds, nil, _value), do: seeds
Expand Down
106 changes: 106 additions & 0 deletions lib/oli/utils/stagehand.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
defmodule Oli.Utils.Stagehand do
@moduledoc """
Stagehand is a tool for generating fake data for testing and development.
## Usage
Be sure to start your development server in iex mode: `iex -S mix phx.server`
Then you can use the following commands to generate fake data:
```elixir
# Simulate enrollments for a section
iex> Oli.Utils.Stagehand.simulate_enrollments("example_section", num_instructors: 3, num_students: 5)
# Simulate progress for students in a section, with 80% of responses being correct
iex> Oli.Utils.Stagehand.simulate_progress("example_section", pct_correct: 0.8)
```
"""

require Logger

alias Oli.Utils.Seeder
alias Oli.Delivery.Sections
alias Oli.Utils.Stagehand.SimulateProgress

@doc """
Simulates a typical set of enrollments for a section.
"""
def simulate_enrollments(section_slug, opts \\ []) do
num_instructors = Keyword.get(opts, :num_instructors, 3)
num_students = Keyword.get(opts, :num_students, 5)

case Sections.get_section_by_slug(section_slug) do
nil ->
Logger.error("Section not found: #{section_slug}")

section ->
map = %{}

map =
if num_instructors > 0 do
map = Enum.reduce(1..num_instructors, map, fn _i, map ->
Seeder.Section.create_and_enroll_instructor(map, section)
end)

Logger.info("Enrolled #{num_instructors} instructors in section #{section_slug}")

map
else
map
end

map =
if num_students > 0 do
map = Enum.reduce(1..num_students, map, fn _i, map ->
Seeder.Section.create_and_enroll_learner(map, section)
end)

Logger.info("Enrolled #{num_students} students in section #{section_slug}")

map
else
map
end

map
end
end

@doc """
Simulates progress for students in a given section.
## Options
* `pct_correct` - the percentage of responses that should be correct, defaults to 1.0
* `chunk_size` - the number of students to simulate at a time, defaults to 10
"""
def simulate_progress(section_slug, opts \\ []) do
pct_correct = Keyword.get(opts, :pct_correct, 1.0)
chunk_size = Keyword.get(opts, :chunk_size, 10)

students =
Sections.fetch_students(section_slug)
|> Enum.map(fn student -> {student, UUID.uuid4()} end)

section = Sections.get_section_by_slug(section_slug)
all_pages = Sections.fetch_all_pages(section_slug)

students
|> Enum.chunk_every(chunk_size)
|> Enum.map(fn chunk ->
chunk
|> Enum.map(fn {student, datashop_session_id} ->
Task.async(fn ->
SimulateProgress.simulate_student_working_through_course(
section,
student,
all_pages,
datashop_session_id,
pct_correct
)
end)
end)
|> Task.await_many()
end)
end

end
Loading

0 comments on commit 92720a5

Please sign in to comment.