diff --git a/.github/workflows/storybook.yml b/.github/workflows/docs.yml similarity index 69% rename from .github/workflows/storybook.yml rename to .github/workflows/docs.yml index 49222b758..94564d7fe 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: Build & Publish Storybook +name: Build & Publish Docs & Storybook on: push: @@ -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/**' @@ -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 diff --git a/backend/experiment/actions/trial.py b/backend/experiment/actions/trial.py index 5e6b27627..edcc03c8c 100644 --- a/backend/experiment/actions/trial.py +++ b/backend/experiment/actions/trial.py @@ -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 @@ -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) @@ -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 diff --git a/backend/result/utils.py b/backend/result/utils.py index 8d47b2c60..bea304683 100644 --- a/backend/result/utils.py +++ b/backend/result/utils.py @@ -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: @@ -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) @@ -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: @@ -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 @@ -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: diff --git a/backend/session/models.py b/backend/session/models.py index 08f31338f..27066c62c 100644 --- a/backend/session/models.py +++ b/backend/session/models.py @@ -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) @@ -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""" @@ -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, @@ -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)) @@ -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() diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 90fbf71bf..9da30e2da 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -30,6 +30,7 @@ const config = { viteFinal: (config) => { return mergeConfig(config, { + base: "/MUSCLE/storybook/", resolve: { alias: { '@/': '/src/', diff --git a/frontend/src/components/Block/Block.tsx b/frontend/src/components/Block/Block.tsx index da40b9a8e..b66a4bb15 100644 --- a/frontend/src/components/Block/Block.tsx +++ b/frontend/src/components/Block/Block.tsx @@ -169,8 +169,6 @@ const Block = () => { const onResult = useResultHandler({ session, participant, - onNext, - state, }); // Render block state diff --git a/frontend/src/components/FeedbackForm/FeedbackForm.tsx b/frontend/src/components/FeedbackForm/FeedbackForm.tsx index 35a8018b4..8dab41173 100644 --- a/frontend/src/components/FeedbackForm/FeedbackForm.tsx +++ b/frontend/src/components/FeedbackForm/FeedbackForm.tsx @@ -12,6 +12,7 @@ interface FeedbackFormProps { skipLabel: string; isSkippable: boolean; onResult: OnResultType + onNext: () => void; emphasizeTitle?: boolean; } @@ -23,6 +24,7 @@ const FeedbackForm = ({ skipLabel, isSkippable, onResult, + onNext, emphasizeTitle = false, }: FeedbackFormProps) => { const isSubmitted = useRef(false); @@ -31,7 +33,7 @@ const FeedbackForm = ({ const [formValid, setFormValid] = useState(false); - const onSubmit = () => { + const onSubmit = async () => { // Prevent double submit if (isSubmitted.current) { return; @@ -39,9 +41,11 @@ const FeedbackForm = ({ isSubmitted.current = true; // Callback onResult with question data - onResult({ + await onResult({ form, }); + + onNext(); }; const onChange = (value: string | number | boolean, question_index: number) => { diff --git a/frontend/src/components/Question/_ButtonArray.test.tsx b/frontend/src/components/Question/_ButtonArray.test.tsx index 8f22a6472..191a1bde8 100644 --- a/frontend/src/components/Question/_ButtonArray.test.tsx +++ b/frontend/src/components/Question/_ButtonArray.test.tsx @@ -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", diff --git a/frontend/src/components/Trial/Trial.test.tsx b/frontend/src/components/Trial/Trial.test.tsx index 9db8e89e5..1c370c490 100644 --- a/frontend/src/components/Trial/Trial.test.tsx +++ b/frontend/src/components/Trial/Trial.test.tsx @@ -13,8 +13,8 @@ vi.mock("../Playback/Playback", () => ({ )), })); vi.mock("../FeedbackForm/FeedbackForm", () => ({ - default: vi.fn(({ onResult }) => ( -