Skip to content

Commit

Permalink
Merge branch 'develop' into feature/translated-content-form
Browse files Browse the repository at this point in the history
  • Loading branch information
BeritJanssen committed Dec 13, 2024
2 parents 15a2739 + 1b5ba68 commit 075529e
Show file tree
Hide file tree
Showing 55 changed files with 1,342 additions and 907 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ jobs:
working-directory: ./frontend

- name: Setup Github Pages
uses: actions/configure-pages@v2
uses: actions/configure-pages@v5

- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: ./docs-pages

- name: Archive production artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: build
path: ./docs-pages
Expand All @@ -70,4 +70,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4
25 changes: 24 additions & 1 deletion backend/docs/11_Exporting_result_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ Using the admin interface you can export all relevant result data from the sessi

To do so, navigate to [localhost:8000/admin/experiment/block](http://localhost:8000/admin/experiment/block) and click the `Export JSON` button next to the block that you want to export.

After downloading and extracting the zip file you will have 6 JSON files containing the raw data as it is stored in the database by Django :
After downloading and extracting the zip file you will have 7 JSON files containing the raw data as it is stored in the database by Django :

- [`sessions.json`](#sessionsjson) - All sessions that were logged by running this block.
- [`participants.json`](#participantsjson) - All participants that started a `Session` with this block.
- [`profiles.json`](#profilesjson) - All profile questions answered by the participants.
- [`results.json`](#resultsjson) - All results from the trials of this block.
- [`sections.json`](#sectionsjson) - All sections (sounds, images, stimuli, etc.) used in this block.
- [`songs.json`](#songsjson) - All `Song` objects that belong to the sections that were used in this block.
- [`feedback.json`](#feedbackjson) - All `Feedback` objects that belong to this block.

### Format of the exported data

Expand Down Expand Up @@ -318,6 +319,28 @@ A list of `Song` objects that belong to the sections of the trials used for this
- `artist`: The artist's name of this `Song`.
- `name`: The name of this `Song`.

***
#### feedback.json

A list of `Feedback` objects that belong to this `Block`.
###### Example of an object of the `Feedback` model:
```
{
"model": "experiment.feedback",
"pk": 1,
"fields": {
"text": "Lorem.",
"block": 3
}
}
```

- `model`: Name of the django app followed by the name of the model (or database table).
- `pk`: Primary Key of this `Feedback` object.
- `fields`:
- `text`: The feedback on this block given by an anonymous participant.
- `block`: Foreign key `fk` relates to the `Block` object.

## Export selected result data in CSV format


Expand Down
82 changes: 57 additions & 25 deletions backend/experiment/actions/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,51 @@

from django.utils.translation import gettext as _

from result.models import Result
from session.models import Session
from .base_action import BaseAction


class Score(BaseAction): # pylint: disable=too-few-public-methods
class Score(BaseAction):
"""
Provide data for an intermediate score view
Provide data for a score view, presenting feedback to a participant after a Trial
Relates to client component: Score.ts
Relates to client component: Score.js
Args:
session: a Session object
title: the title of the score page
result: the result for which section and/or score should be reported
score: the score to report, will override result.score
score_message: a function which constructs feedback text based on the score
config: a dict with the following settings:
- show_section: whether metadata of the previous section should be shown
- show_total_score: whether the total score should be shown
icon: the name of a themify-icon shown with the view or None
timer: int or None. If int, wait for as many seconds until showing the next view
feedback: An additional feedback text
"""

ID = 'SCORE'

def __init__(self, session, title: str = None, score=None, score_message=None, config=None, icon=None, timer=None, feedback=None):
""" Score presents feedback to a participant after a Trial
- session: a Session object
- title: the title of the score page
- score_message: a function which constructs feedback text based on the score
- config: a dict with the following settings:
- show_section: whether metadata of the previous section should be shown
- show_total_score: whether the total score should be shown
- icon: the name of a themify-icon shown with the view or None
- timer: int or None. If int, wait for as many seconds until showing the next view
- feedback: An additional feedback text
"""
self.session = session
def __init__(
self,
session: Session,
title: str = '',
result: Result = None,
score: float = None,
score_message: str = '',
config: dict = {},
icon: str = None,
timer: int = None,
feedback: str = None,
):
self.title = title or _('Round {get_rounds_passed} / {total_rounds}').format(
get_rounds_passed=session.get_rounds_passed(),
total_rounds=self.session.block.rounds
total_rounds=session.block.rounds,
)
self.score = score or session.last_score()
self.score_message = score_message or self.default_score_message
self.session = session
self.score = self.get_score(score, result)
self.score_message = score_message or self.default_score_message(self.score)
self.feedback = feedback
self.config = {
'show_section': False,
Expand All @@ -46,30 +60,48 @@ def __init__(self, session, title: str = None, score=None, score_message=None, c
'next': _('Next'),
'listen_explainer': _('You listened to:')
}
self.last_song = result.section.song_label() if result else session.last_song()
self.timer = timer

def action(self):
"""Serialize score data"""
def action(self) -> dict:
"""Serialize score data
Returns:
dictionary with the relevant data for the Score.ts view
"""
# Create action
action = {
'view': self.ID,
'title': self.title,
'score': self.score,
'score_message': self.score_message(self.score),
'score_message': self.score_message,
'texts': self.texts,
'feedback': self.feedback,
'icon': self.icon,
'timer': self.timer
'timer': self.timer,
}
if self.config['show_section']:
action['last_song'] = self.session.last_song()
action['last_song'] = self.last_song
if self.config['show_total_score']:
action['total_score'] = self.session.total_score()
return action

def get_score(self, score: float = None, result: Result = None) -> float:
"""Retrieve the last relevant score, fall back to session.last_score() if neither score nor result are defined
Args:
score: the score passed from the rules file (optional)
result: a Result object passed from the rules file (opional)
"""
if score:
return score
elif result:
return result.score
else:
return self.session.last_score()

def default_score_message(self, score):
"""Fallback to generate a message for the given score"""

# None
if score is None:
score = 0
Expand Down
8 changes: 4 additions & 4 deletions backend/experiment/actions/tests/test_actions_score.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ def test_initialization_full_parameters(self):
session=self.mock_session,
title="Test Title",
score=100,
score_message=lambda x: f"Score is {x}",
score_message="Score is 100",
config={'show_section': True, 'show_total_score': True},
icon="icon-test",
timer=5,
feedback="Test Feedback"
feedback="Test Feedback",
)
self.assertEqual(score.title, "Test Title")
self.assertEqual(score.score, 100)
self.assertEqual(score.score_message(score.score), "Score is 100")
self.assertEqual(score.score_message, "Score is 100")
self.assertEqual(score.feedback, "Test Feedback")
self.assertEqual(
score.config, {'show_section': True, 'show_total_score': True})
Expand All @@ -43,7 +43,7 @@ def test_initialization_minimal_parameters(self):
score = Score(session=self.mock_session)
self.assertIn('Round', score.title)
self.assertEqual(score.score, 10)
self.assertEqual(score.score_message, score.default_score_message)
self.assertEqual(score.score_message, score.default_score_message(score.score))
self.assertIsNone(score.feedback)
self.assertEqual(
score.config, {'show_section': False, 'show_total_score': False})
Expand Down
25 changes: 13 additions & 12 deletions backend/experiment/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,7 @@
from question.models import QuestionSeries, QuestionInSeries


class FeedbackInline(admin.TabularInline):
"""Inline to show results linked to given participant"""

model = Feedback
fields = ["text"]
extra = 0


class BlockTranslatedContentInline(NestedStackedInline):
class BlockTranslatedContentInline(NestedTabularInline):
model = BlockTranslatedContent
form = BlockTranslatedContentForm
template = "admin/translated_content.html"
Expand Down Expand Up @@ -98,7 +90,7 @@ class BlockAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin):
"bonus_points",
"playlists",
]
inlines = [QuestionSeriesInline, FeedbackInline, BlockTranslatedContentInline]
inlines = [QuestionSeriesInline, BlockTranslatedContentInline]
form = BlockForm

# make playlists fields a list of checkboxes
Expand All @@ -115,6 +107,7 @@ def export(self, request, obj, parent_obj=None):
all_sections = Section.objects.none()
all_participants = Participant.objects.none()
all_profiles = Result.objects.none()
all_feedback = Feedback.objects.filter(block=obj)

# Collect data
all_sessions = obj.export_sessions().order_by("pk")
Expand Down Expand Up @@ -170,6 +163,10 @@ def export(self, request, obj, parent_obj=None):
"songs.json",
data=str(serializers.serialize("json", all_songs.order_by("pk"))),
)
new_zip.writestr(
"feedback.json",
data=str(serializers.serialize("json", all_feedback.order_by("pk"))),
)

# create forced download response
response = HttpResponse(zip_buffer.getbuffer())
Expand Down Expand Up @@ -387,9 +384,9 @@ def duplicate(self, request, obj, parent_obj=None):
)

# order_by is inserted here to prevent a query error
exp_contents = obj.translated_content.order_by('name').all()
exp_contents = obj.translated_content.order_by("name").all()
# order_by is inserted here to prevent a query error
exp_phases = obj.phases.order_by('index').all()
exp_phases = obj.phases.order_by("index").all()

# Duplicate Experiment object
exp_copy = obj
Expand Down Expand Up @@ -478,15 +475,18 @@ def experimenter_dashboard(self, request, obj, parent_obj=None):
all_blocks = obj.associated_blocks()
all_participants = obj.current_participants()
all_sessions = obj.export_sessions()
all_feedback = obj.export_feedback()
collect_data = {
"participant_count": len(all_participants),
"session_count": len(all_sessions),
"feedback_count": len(all_feedback),
}

blocks = [
{
"id": block.id,
"slug": block.slug,
"name": block,
"started": len(all_sessions.filter(block=block)),
"finished": len(
all_sessions.filter(
Expand All @@ -508,6 +508,7 @@ def experimenter_dashboard(self, request, obj, parent_obj=None):
"blocks": blocks,
"sessions": all_sessions,
"participants": all_participants,
"feedback": all_feedback,
"collect_data": collect_data,
},
)
Expand Down
4 changes: 2 additions & 2 deletions backend/experiment/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ class Meta:
}

class Media:
js = ["block_admin.js"]
css = {"all": ["block_admin.css"]}
js = ["block_admin.js", "collapsible_blocks.js"]
css = {"all": ["block_admin.css", "collapsible_blocks.css"]}


class ExportForm(Form):
Expand Down
17 changes: 17 additions & 0 deletions backend/experiment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def __str__(self):
translated_content = self.get_fallback_content()
return translated_content.name if translated_content else self.slug

@property
def name(self):
content = self.get_fallback_content()
return content.name if content and content.name else ""

class Meta:
verbose_name_plural = "Experiments"

Expand Down Expand Up @@ -81,6 +86,18 @@ def current_participants(self) -> list["Participant"]:
participants[session.participant.id] = session.participant
return participants.values()

def export_feedback(self) -> QuerySet[Session]:
"""export feedback for the blocks in this experiment
Returns:
Associated block feedback
"""

all_feedback = Feedback.objects.none()
for block in self.associated_blocks():
all_feedback |= Feedback.objects.filter(block=block)
return all_feedback

def get_fallback_content(self) -> "ExperimentTranslatedContent":
"""Get fallback content for the experiment
Expand Down
2 changes: 1 addition & 1 deletion backend/experiment/rules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def get_play_again_url(self, session: Session):
if session.participant.participant_id_url
else ""
)
return f"/{session.block.slug}{participant_id_url_param}"
return f"/block/{session.block.slug}{participant_id_url_param}"

def calculate_intermediate_score(self, session, result):
"""process result data during a trial (i.e., between next_round calls)
Expand Down
6 changes: 3 additions & 3 deletions backend/experiment/rules/hooked.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@ def next_heard_before_action(self, session: Session, round_number: int) -> Trial
)
return trial

def get_score(self, session: Session, round_number: int):
def get_score(self, session: Session, round_number: int) -> Score:
config = {"show_section": True, "show_total_score": True}
title = self.get_trial_title(session, round_number)
previous_score = session.last_result(self.counted_result_keys).score
return Score(session, config=config, title=title, score=previous_score)
previous_result = session.last_result(self.counted_result_keys)
return Score(session, config=config, title=title, result=previous_result)
Loading

0 comments on commit 075529e

Please sign in to comment.