Skip to content

Commit

Permalink
[FEATURE] [MER-3873] Likert activity for instructors (#5274)
Browse files Browse the repository at this point in the history
* parse data for likert graph + placeholder for likert activity

* add datasets and vegalite specs to render likert

* refactor: move queries to context modules +  move common logic into new ActivityHelpers

* move rendered_activity function component to ActivityHelpers module

* implement new likert visualization in scored activities

* implement new likert visualization in surveys

* remove unused alias

* fix test warning

* add dataset and preview_rendered when section is :v2

* remove unused aliases

* consider analytics version v2 for rendering likert visualization

* add and update tests

* add dark/light mode support + finish styling

* sort questions on Y axis + consider blank response case

* private functions to explicit magic-numbers + more styling

* fix warning
  • Loading branch information
nicocirio authored Dec 2, 2024
1 parent 25507c5 commit 549032c
Show file tree
Hide file tree
Showing 11 changed files with 1,296 additions and 1,469 deletions.
65 changes: 63 additions & 2 deletions assets/src/components/misc/VegaLiteRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,75 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { VegaLite, VisualizationSpec } from 'react-vega';

export interface VegaLiteSpec {
spec: VisualizationSpec;
}

export const VegaLiteRenderer = (props: VegaLiteSpec) => {
const [darkMode, setDarkMode] = useState(document.documentElement.classList.contains('dark'));

const viewRef = useRef<any>(null);

// Update the 'isDarkMode' parameter and background color when 'darkMode' changes
useEffect(() => {
if (viewRef.current) {
const view = viewRef.current;

view.signal('isDarkMode', darkMode);
view.background(darkMode ? '#262626' : 'white');
view.run();
}
}, [darkMode]);

const darkTooltipTheme = {
theme: 'dark',
style: {
'vega-tooltip': {
backgroundColor: 'black',
color: 'white',
},
},
};
const lightTooltipTheme = {
theme: 'light',
style: {
'vega-tooltip': {
backgroundColor: 'white',
color: 'black',
},
},
};

// Set up a MutationObserver to listen for changes to the 'class' attribute
useEffect(() => {
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('dark');
setDarkMode(isDark);
});

observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});

return () => {
observer.disconnect();
};
}, []);

return (
<>
<VegaLite spec={props.spec} actions={false} />
<VegaLite
spec={props.spec}
actions={false}
tooltip={darkMode ? darkTooltipTheme : lightTooltipTheme}
onNewView={(view) => {
viewRef.current = view;
view.signal('isDarkMode', darkMode);
view.background(darkMode ? '#262626' : 'white');
view.run();
}}
/>
</>
);
};
59 changes: 59 additions & 0 deletions lib/oli/analytics/summary.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
defmodule Oli.Analytics.Summary do
import Ecto.Query

alias Oli.Analytics.Summary.{
AttemptGroup,
ResponseLabel,
Expand All @@ -10,6 +12,9 @@ defmodule Oli.Analytics.Summary do

alias Oli.Analytics.Common.Pipeline
alias Oli
alias Oli.Repo
alias Oli.Resources.ResourceType

require Logger

@resource_fields "project_id, publication_id, section_id, user_id, resource_id, part_id, resource_type_id, num_correct, num_attempts, num_hints, num_first_attempts, num_first_attempts_correct"
Expand Down Expand Up @@ -409,4 +414,58 @@ defmodule Oli.Analytics.Summary do
|> StudentResponse.changeset(attrs)
|> Oli.Repo.insert()
end

@doc """
Counts the number of attempts made by a list of students for a given activity in a given section.
"""
@spec count_student_attempts(
activity_resource_id :: integer(),
section_id :: integer(),
student_ids :: [integer()]
) :: integer() | nil
def count_student_attempts(activity_resource_id, section_id, student_ids) do
page_type_id = ResourceType.get_id_by_type("activity")

from(rs in ResourceSummary,
where:
rs.section_id == ^section_id and rs.resource_id == ^activity_resource_id and
rs.user_id in ^student_ids and rs.project_id == -1 and rs.publication_id == -1 and
rs.resource_type_id == ^page_type_id,
select: sum(rs.num_attempts)
)
|> Repo.one()
end

@doc """
Returns a list of response summaries for a given page resource id, section id, and activity resource ids.
"""
@spec get_response_summary_for(
page_resource_id :: integer(),
section_id :: integer(),
activity_resource_ids :: [integer()]
) :: [map()]
def get_response_summary_for(page_resource_id, section_id, activity_resource_ids) do
from(rs in ResponseSummary,
join: rpp in ResourcePartResponse,
on: rs.resource_part_response_id == rpp.id,
left_join: sr in StudentResponse,
on:
rs.section_id == sr.section_id and rs.page_id == sr.page_id and
rs.resource_part_response_id == sr.resource_part_response_id,
left_join: u in Oli.Accounts.User,
on: sr.user_id == u.id,
where:
rs.section_id == ^section_id and rs.page_id == ^page_resource_id and
rs.publication_id == -1 and rs.project_id == -1 and
rs.activity_id in ^activity_resource_ids,
select: %{
part_id: rpp.part_id,
response: rpp.response,
count: rs.count,
user: u,
activity_id: rs.activity_id
}
)
|> Repo.all()
end
end
103 changes: 103 additions & 0 deletions lib/oli/delivery/attempts/core.ex
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,93 @@ defmodule Oli.Delivery.Attempts.Core do
{:ok, results}
end

@doc """
Returns a list of activity attempts for a given section and activity resource ids.
"""
@spec get_activity_attempts_by(integer(), [integer()]) :: [map()]
def get_activity_attempts_by(section_id, activity_resource_ids) do
from(activity_attempt in ActivityAttempt,
left_join: resource_attempt in assoc(activity_attempt, :resource_attempt),
left_join: resource_access in assoc(resource_attempt, :resource_access),
left_join: user in assoc(resource_access, :user),
left_join: activity_revision in assoc(activity_attempt, :revision),
left_join: resource_revision in assoc(resource_attempt, :revision),
where:
resource_access.section_id == ^section_id and
activity_revision.resource_id in ^activity_resource_ids,
select: activity_attempt,
select_merge: %{
activity_type_id: activity_revision.activity_type_id,
activity_title: activity_revision.title,
page_title: resource_revision.title,
page_id: resource_revision.resource_id,
resource_attempt_number: resource_attempt.attempt_number,
graded: resource_revision.graded,
user: user,
revision: activity_revision,
resource_attempt_guid: resource_attempt.attempt_guid,
resource_access_id: resource_access.id
}
)
|> Repo.all()
end

@doc """
Fetches the activities for a given assessment and section attempted by a list of students
## Parameters
* `assessment_resource_id` - the resource id of the assessment
* `section_id` - the section id
* `student_ids` - the list of student ids
* `filter_by_survey` - an optional boolean flag to filter by survey activities
iex> get_activities(1, 2, [3, 4], false)
[
%{
id: 1,
resource_id: 1,
title: "Activity 1"
},
%{
id: 2,
resource_id: 2,
title: "Activity 2"
}
]
"""
@spec get_evaluated_activities_for(integer(), integer(), [integer()], boolean()) :: [map()]
def get_evaluated_activities_for(
assessment_resource_id,
section_id,
student_ids,
filter_by_survey \\ false
) do
filter_by_survey? =
if filter_by_survey do
dynamic([aa, _], not is_nil(aa.survey_id))
else
dynamic([aa, _], is_nil(aa.survey_id))
end

from(aa in ActivityAttempt,
join: res_attempt in ResourceAttempt,
on: aa.resource_attempt_id == res_attempt.id,
where: aa.lifecycle_state == :evaluated,
join: res_access in ResourceAccess,
on: res_attempt.resource_access_id == res_access.id,
where:
res_access.section_id == ^section_id and
res_access.resource_id == ^assessment_resource_id and
res_access.user_id in ^student_ids,
where: ^filter_by_survey?,
join: rev in Revision,
on: aa.revision_id == rev.id,
group_by: [rev.resource_id, rev.id],
select: map(rev, [:id, :resource_id, :title])
)
|> Repo.all()
end

def get_all_activity_attempts(resource_attempt_id) do
Repo.all(
from(activity_attempt in ActivityAttempt,
Expand Down Expand Up @@ -919,4 +1006,20 @@ defmodule Oli.Delivery.Attempts.Core do
)
)
end

@doc """
Counts the number of attempts made by a list of students for a given activity in a given section.
"""
@spec count_student_attempts(integer(), %Section{}, [integer()]) :: integer() | nil
def count_student_attempts(activity_resource_id, section_id, student_ids) do
from(ra in ResourceAttempt,
join: access in ResourceAccess,
on: access.id == ra.resource_access_id,
where:
ra.lifecycle_state == :evaluated and access.section_id == ^section_id and
access.resource_id == ^activity_resource_id and access.user_id in ^student_ids,
select: count(ra.id)
)
|> Repo.one()
end
end
Loading

0 comments on commit 549032c

Please sign in to comment.