Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/Amsterdam-Music-Lab/MUSCLE
Browse files Browse the repository at this point in the history
… into develop
  • Loading branch information
BeritJanssen committed Sep 23, 2024
2 parents b2ac938 + 85f2743 commit 9152c6f
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 219 deletions.
23 changes: 17 additions & 6 deletions .github/workflows/storybook.yml → .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build & Publish Storybook
name: Build & Publish Docs & Storybook

on:
push:
Expand All @@ -7,13 +7,13 @@ on:
- develop
paths:
- 'frontend/**'
- '.github/workflows/storybook.yml'
- '.github/workflows/docs.yml'
- '.yarn/**'
- '.storybook/**'
pull_request:
paths:
- 'frontend/**'
- '.github/workflows/storybook.yml'
- '.github/workflows/docs.yml'
- '.yarn/**'
- '.storybook/**'

Expand All @@ -27,23 +27,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build ER/MKDOCS site
run: |
mkdir -p docs-pages
sudo docker compose --env-file .env-github-actions run server bash -c "mkdocs build -d ./docs-pages-build"
cp -r ./backend/docs-pages-build/* ./docs-pages
- name: Install dependencies
run: yarn
working-directory: ./frontend

- name: Build Storybook
run: yarn storybook:build
run: yarn storybook:build --output-dir ../docs-pages/storybook
working-directory: ./frontend

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

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

- name: Archive production artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: ./frontend/storybook-static
path: ./docs-pages

deploy-gh-pages:
# only deploy on develop branch
Expand Down
51 changes: 23 additions & 28 deletions backend/experiment/actions/trial.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,24 @@ class Trial(BaseAction): # pylint: disable=too-few-public-methods
- playback: player(s) to be displayed in this view
- feedback_form: array of form elements
- title: page title - defaults to empty
- result_id: optional, a result_id with which the whole Trial is associated
"""

ID = 'TRIAL_VIEW'
ID = "TRIAL_VIEW"

def __init__(
self,
playback: Playback = None,
html: str = None,
feedback_form: Form = None,
title='',
config: dict = None,
result_id: int = None,
style: FrontendStyle = FrontendStyle()
):
'''
self,
playback: Playback = None,
html: str = None,
feedback_form: Form = None,
title="",
config: dict = None,
style: FrontendStyle = FrontendStyle(),
):
"""
- playback: Playback object (may be None)
- html: HTML object (may be None)
- feedback_form: Form object (may be None)
- title: string setting title in header of experiment
- result_id: id of Result to handle (especially important if there's no feedback form)
- config: dictionary with following settings
- response_time: how long to wait until stopping the player / proceeding to the next view
- auto_advance: proceed to next view after player has stopped
Expand All @@ -47,18 +44,17 @@ def __init__(
- neutral-inverted: first element is yellow, second is blue, third is teal
- boolean: first element is green, second is red
- boolean-negative-first: first element is red, second is green
'''
"""
self.playback = playback
self.html = html
self.feedback_form = feedback_form
self.title = title
self.result_id = result_id
self.config = {
'response_time': 5,
'auto_advance': False,
'listen_first': False,
'show_continue_button': True,
'continue_label': _('Continue'),
"response_time": 5,
"auto_advance": False,
"listen_first": False,
"show_continue_button": True,
"continue_label": _("Continue"),
}
if config:
self.config.update(config)
Expand All @@ -71,18 +67,17 @@ def action(self):
"""
# Create action
action = {
'view': Trial.ID,
'title': self.title,
'config': self.config,
'result_id': self.result_id,
"view": Trial.ID,
"title": self.title,
"config": self.config,
}
if self.style:
action['style'] = self.style.to_dict()
action["style"] = self.style.to_dict()
if self.playback:
action['playback'] = self.playback.action()
action["playback"] = self.playback.action()
if self.html:
action['html'] = self.html.action()
action["html"] = self.html.action()
if self.feedback_form:
action['feedback_form'] = self.feedback_form.action()
action["feedback_form"] = self.feedback_form.action()

return action
32 changes: 10 additions & 22 deletions backend/result/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


def get_result(session, data):
result_id = data.get('result_id')
result_id = data.get("result_id")
try:
result = Result.objects.get(pk=result_id, session=session)
except Result.DoesNotExist:
Expand All @@ -23,12 +23,7 @@ def handle_results(data, session):
if the given_result is an array of results, retrieve and save results for all of them
else, handle results at top level
"""
try:
form = data.pop('form')
except KeyError:
# no form, handle results at top level
result = score_result(data, session)
return result
form = data.pop("form")
for form_element in form:
result = get_result(session, form_element)
# save relevant data such as config and decision time (except for the popped form)
Expand All @@ -39,26 +34,23 @@ def handle_results(data, session):


def prepare_profile_result(question_key, participant, **kwargs):
''' Create a Result object, and provide its id to be serialized
"""Create a Result object, and provide its id to be serialized
- question_key: the key of the question in the questionnaire dictionaries
- participant: the participant on which the Result is going to be registered
possible kwargs:
- expected_response: optionally, provide the correct answer, used for scoring
- comment: optionally, provide a comment to be saved in the database
- scoring_rule: optionally, provide a scoring rule
'''
scoring_rule = PROFILE_SCORING_RULES.get(question_key, '')
"""
scoring_rule = PROFILE_SCORING_RULES.get(question_key, "")
result, created = Result.objects.get_or_create(
question_key=question_key,
participant=participant,
scoring_rule=scoring_rule,
**kwargs
question_key=question_key, participant=participant, scoring_rule=scoring_rule, **kwargs
)
return result


def prepare_result(question_key: str, session: Session, **kwargs) -> int:
''' Create a Result object, and provide its id to be serialized
"""Create a Result object, and provide its id to be serialized
- question_key: the key of the question in the questionnaire dictionaries
- session: the session on which the Result is going to be registered
possible kwargs:
Expand All @@ -67,12 +59,8 @@ def prepare_result(question_key: str, session: Session, **kwargs) -> int:
- json_data: optionally, provide json data tied to this result
- comment: optionally, provide a comment to be saved in the database, e.g. "training phase"
- scoring_rule: optionally, provide a scoring rule
'''
result = Result.objects.create(
question_key=question_key,
session=session,
**kwargs
)
"""
result = Result.objects.create(question_key=question_key, session=session, **kwargs)
return result.id


Expand All @@ -90,7 +78,7 @@ def score_result(data, session):
"""
result = get_result(session, data)
result.save_json_data(data)
result.given_response = data.get('value')
result.given_response = data.get("value")
# Calculate score: by default, apply a scoring rule
# Can be overridden by defining calculate_score in the rules file
if result.session:
Expand Down
27 changes: 11 additions & 16 deletions backend/session/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,12 @@
class Session(models.Model):
"""Experiment session by a participant"""

block = models.ForeignKey(
"experiment.Block", on_delete=models.CASCADE, blank=True, null=True
)
block = models.ForeignKey("experiment.Block", on_delete=models.CASCADE, blank=True, null=True)
participant = models.ForeignKey("participant.Participant", on_delete=models.CASCADE)
playlist = models.ForeignKey(
"section.Playlist", on_delete=models.SET_NULL, blank=True, null=True
)
playlist = models.ForeignKey("section.Playlist", on_delete=models.SET_NULL, blank=True, null=True)

started_at = models.DateTimeField(db_index=True, default=timezone.now)
finished_at = models.DateTimeField(
db_index=True, default=None, null=True, blank=True
)
finished_at = models.DateTimeField(db_index=True, default=None, null=True, blank=True)
json_data = models.JSONField(default=dict, blank=True, null=True)
final_score = models.FloatField(db_index=True, default=0.0)
current_round = models.IntegerField(default=1)
Expand All @@ -35,9 +29,7 @@ def result_count(self):
def total_score(self):
"""Sum of all result scores"""
score = self.result_set.aggregate(models.Sum("score"))
return self.block.bonus_points + (
score["score__sum"] if score["score__sum"] else 0
)
return self.block.bonus_points + (score["score__sum"] if score["score__sum"] else 0)

def last_score(self):
"""Get last score, or return 0 if no scores are set"""
Expand All @@ -49,7 +41,9 @@ def last_score(self):

def last_result(self):
"""Get last result"""
return self.result_set.last()
result = self.result_set.last()

return Result.objects.get(pk=result.id)

def last_song(self):
"""Return artist and name of previous song,
Expand Down Expand Up @@ -122,8 +116,9 @@ def get_used_song_ids(self, exclude={}):
def get_unused_song_ids(self, filter_by={}):
"""Get a list of unused song ids from this session's playlist"""
# Get all song ids from the current playlist
song_ids = self.playlist.section_set.filter(
**filter_by).order_by('song').values_list('song_id', flat=True).distinct()
song_ids = (
self.playlist.section_set.filter(**filter_by).order_by("song").values_list("song_id", flat=True).distinct()
)
# Get all song ids from results
used_song_ids = self.get_used_song_ids()
return list(set(song_ids) - set(used_song_ids))
Expand Down Expand Up @@ -162,4 +157,4 @@ def get_previous_result(self, question_keys: list = []) -> Result:
results = self.result_set
if question_keys:
results = results.filter(question_key__in=question_keys)
return results.order_by('-created_at').first()
return results.order_by("-created_at").first()
1 change: 1 addition & 0 deletions frontend/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const config = {

viteFinal: (config) => {
return mergeConfig(config, {
base: "/MUSCLE/storybook/",
resolve: {
alias: {
'@/': '/src/',
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/Block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,6 @@ const Block = () => {
const onResult = useResultHandler({
session,
participant,
onNext,
state,
});

// Render block state
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/FeedbackForm/FeedbackForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface FeedbackFormProps {
skipLabel: string;
isSkippable: boolean;
onResult: OnResultType
onNext: () => void;
emphasizeTitle?: boolean;
}

Expand All @@ -23,6 +24,7 @@ const FeedbackForm = ({
skipLabel,
isSkippable,
onResult,
onNext,
emphasizeTitle = false,
}: FeedbackFormProps) => {
const isSubmitted = useRef(false);
Expand All @@ -31,17 +33,19 @@ const FeedbackForm = ({

const [formValid, setFormValid] = useState(false);

const onSubmit = () => {
const onSubmit = async () => {
// Prevent double submit
if (isSubmitted.current) {
return;
}
isSubmitted.current = true;

// Callback onResult with question data
onResult({
await onResult({
form,
});

onNext();
};

const onChange = (value: string | number | boolean, question_index: number) => {
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/Question/_ButtonArray.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const getProps = (overrides = {}) => ({
"view": QuestionViews.BUTTON_ARRAY,
"explainer": "",
"question": "1. Do you know this song?",
"result_id": 12345,
"is_skippable": false,
"submits": false,
"style": "boolean",
Expand Down
Loading

0 comments on commit 9152c6f

Please sign in to comment.