diff --git a/backend/experiment/rules/base.py b/backend/experiment/rules/base.py index a11e4fc22..feffaa402 100644 --- a/backend/experiment/rules/base.py +++ b/backend/experiment/rules/base.py @@ -5,12 +5,12 @@ from django.conf import settings from experiment.actions import Final, Form, Trial -from experiment.models import Experiment from section.models import Playlist from experiment.questions.demographics import DEMOGRAPHICS from experiment.questions.goldsmiths import MSI_OTHER from experiment.questions.utils import question_by_key, unanswered_questions from result.score import SCORING_RULES +from session.models import Session from experiment.questions import get_questions_from_keys @@ -52,7 +52,11 @@ def calculate_score(self, result, data): if scoring_rule: return scoring_rule(result, data) return None - + + def get_play_again_url(self, session: Session): + participant_id_url_param = f'?participant_id={session.participant.participant_id_url}' if session.participant.participant_id_url else "" + return f'/{session.experiment.slug}{participant_id_url_param}' + def calculate_intermediate_score(self, session, result): """ process result data during a trial (i.e., between next_round calls) return score diff --git a/backend/experiment/rules/hooked.py b/backend/experiment/rules/hooked.py index dc97fd4bf..aa0c9ace4 100644 --- a/backend/experiment/rules/hooked.py +++ b/backend/experiment/rules/hooked.py @@ -111,8 +111,10 @@ def next_round(self, session): social=self.social_media_info( session.experiment, total_score), show_profile_link=True, - button={'text': _('Play again'), 'link': '{}/{}'.format( - settings.CORS_ORIGIN_WHITELIST[0], session.experiment.slug)} + button={ + 'text': _('Play again'), + 'link': self.get_play_again_url(session), + } ) ] diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index 76b8de1e9..739601d2e 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -81,7 +81,7 @@ def next_round(self, session): final_text='Can you score higher than your friends and family? Share and let them try!', button={ 'text': 'Play again', - 'link': f'/{session.experiment.slug}' + 'link': self.get_play_again_url(session) }, rank=self.rank(session, exclude_unfinished=False), social=social_info, diff --git a/backend/experiment/rules/tests/test_base.py b/backend/experiment/rules/tests/test_base.py index 6fbcdf164..1e96d1f2e 100644 --- a/backend/experiment/rules/tests/test_base.py +++ b/backend/experiment/rules/tests/test_base.py @@ -1,6 +1,8 @@ from django.test import TestCase from django.conf import settings from experiment.models import Experiment +from session.models import Session +from participant.models import Participant from section.models import Playlist from ..base import Base @@ -29,6 +31,35 @@ def test_social_media_info(self): self.assertNotIn(social_media_info['url'], '//') self.assertEqual(social_media_info['hashtags'], ['music-lab', 'amsterdammusiclab', 'citizenscience']) + def test_get_play_again_url(self): + experiment = Experiment.objects.create( + name='Music Lab', + slug='music-lab', + ) + session = Session.objects.create( + experiment=experiment, + participant=Participant.objects.create(), + ) + base = Base() + play_again_url = base.get_play_again_url(session) + self.assertEqual(play_again_url, '/music-lab') + + def test_get_play_again_url_with_participant_id(self): + experiment = Experiment.objects.create( + name='Music Lab', + slug='music-lab', + ) + participant = Participant.objects.create( + participant_id_url='42', + ) + session = Session.objects.create( + experiment=experiment, + participant=participant, + ) + base = Base() + play_again_url = base.get_play_again_url(session) + self.assertEqual(play_again_url, '/music-lab?participant_id=42') + def test_validate_playlist(self): base = Base() playlist = None diff --git a/backend/experiment/rules/thats_my_song.py b/backend/experiment/rules/thats_my_song.py index ab92ce0ba..59a4285a9 100644 --- a/backend/experiment/rules/thats_my_song.py +++ b/backend/experiment/rules/thats_my_song.py @@ -80,8 +80,10 @@ def next_round(self, session): rank=self.rank(session), social=social_info, show_profile_link=True, - button={'text': _('Play again'), 'link': '{}/{}{}'.format(settings.CORS_ORIGIN_WHITELIST[0], session.experiment.slug, - '?participant_id='+session.participant.participant_id_url if session.participant.participant_id_url else '')}, + button={ + 'text': _('Play again'), + 'link': self.get_play_again_url(session) + }, logo={'image': '/images/vumc_mcl_logo.png', 'link':'https://www.vumc.org/music-cognition-lab/welcome'} ) ] @@ -153,4 +155,4 @@ def next_round(self, session): actions.append( self.next_heard_before_action(session)) - return actions \ No newline at end of file + return actions diff --git a/backend/experiment/rules/visual_matching_pairs.py b/backend/experiment/rules/visual_matching_pairs.py index 23c355429..da9c4d69d 100644 --- a/backend/experiment/rules/visual_matching_pairs.py +++ b/backend/experiment/rules/visual_matching_pairs.py @@ -55,7 +55,7 @@ def first_round(self, experiment): playlist, explainer ] - + def next_round(self, session): if session.rounds_passed() < 1: trials = self.get_questionnaire(session) @@ -80,6 +80,7 @@ def next_round(self, session): final_text='Can you score higher than your friends and family? Share and let them try!', button={ 'text': 'Play again', + 'link': self.get_play_again_url(session) }, rank=self.rank(session, exclude_unfinished=False), social=social_info, @@ -88,7 +89,7 @@ def next_round(self, session): cont = self.get_visual_matching_pairs_trial(session) return [score, cont] - + def get_visual_matching_pairs_trial(self, session): player_sections = list(session.playlist.section_set.filter(tag__contains='vmp')) @@ -112,5 +113,5 @@ def calculate_score(self, result, data): for m in moves: m['filename'] = str(Section.objects.get(pk=m.get('selectedSection')).filename) score = data.get('result').get('score') - - return score \ No newline at end of file + + return score diff --git a/frontend/src/components/Experiment/Experiment.jsx b/frontend/src/components/Experiment/Experiment.jsx index 4a3cb5f99..cdcf64b62 100644 --- a/frontend/src/components/Experiment/Experiment.jsx +++ b/frontend/src/components/Experiment/Experiment.jsx @@ -93,7 +93,7 @@ const Experiment = ({ match }) => { }; // trigger next action from next_round array, or call session/next_round - const onNext = async (doBreak) => { + const onNext = async (doBreak = false) => { if (!doBreak && actions.length) { updateActions(actions); } else { diff --git a/frontend/src/components/Final/Final.jsx b/frontend/src/components/Final/Final.jsx index f8da6e60f..43996aa11 100644 --- a/frontend/src/components/Final/Final.jsx +++ b/frontend/src/components/Final/Final.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { Link, withRouter } from "react-router-dom"; +import { withRouter } from "react-router-dom"; import Rank from "../Rank/Rank"; import Social from "../Social/Social"; @@ -9,6 +9,7 @@ import { finalizeSession } from "../../API"; import useBoundStore from "../../util/stores"; import ParticipantLink from "../ParticipantLink/ParticipantLink"; import UserFeedback from "../UserFeedback/UserFeedback"; +import FinalButton from "./FinalButton"; // Final is an experiment view that shows the final scores of the experiment // It can only be the last view of an experiment @@ -65,9 +66,10 @@ const Final = ({ experiment, participant, score, final_text, action_texts, butto {button && (
- - {button.text} - +
)} {logo && ( diff --git a/frontend/src/components/Final/Final.test.jsx b/frontend/src/components/Final/Final.test.jsx index cbfe552c1..74cb195df 100644 --- a/frontend/src/components/Final/Final.test.jsx +++ b/frontend/src/components/Final/Final.test.jsx @@ -67,7 +67,7 @@ describe('Final Component', () => { ); - fireEvent.click(screen.getByText('Next')); + fireEvent.click(screen.getByTestId('button')); await waitFor(() => { expect(onNextMock).toHaveBeenCalled(); }); @@ -134,4 +134,32 @@ session="session-id" expect(API.finalizeSession).toHaveBeenCalledWith({ session: 1, participant: 'participant-id' }); }); + + it('Uses Link to navigate when button link is relative', () => { + render( + + + + ); + + const el = screen.getByTestId('button-link'); + expect(el).to.exist; + expect(el.getAttribute('href')).toBe('/aml'); + }); + + it('Uses an anchor tag to navigate when button link is absolute', () => { + render( + + + + ); + + const el = screen.getByTestId('button-link'); + expect(el).to.exist; + expect(el.getAttribute('href')).toBe('https://example.com'); + }); }); diff --git a/frontend/src/components/Final/FinalButton.tsx b/frontend/src/components/Final/FinalButton.tsx new file mode 100644 index 000000000..816b1db6a --- /dev/null +++ b/frontend/src/components/Final/FinalButton.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Link, withRouter } from "react-router-dom"; + +interface FinalButtonProps { + button: { + text: string; + link: string; + }; + onNext: (value: boolean) => void; +} + +const isRelativeUrl = (url: string) => { + return url && url.startsWith("/"); +} + +const FinalButton: React.FC = ({ button, onNext }) => { + + if (!button) { + return null; + } + + // If the button does not have a link, it will call the onNext function using a button click event + if (!button.link) { + return ( + + ) + } + + // If the button has a link, it will render a Link component if the link is a relative URL + if (isRelativeUrl(button.link)) { + return ( + + {button.text} + + ) + } + + // If the button has a link, it will render an anchor tag if the link is an absolute URL + return ( + + {button.text} + + ) +} + +export default withRouter(FinalButton); \ No newline at end of file diff --git a/frontend/src/stories/Final.stories.jsx b/frontend/src/stories/Final.stories.jsx index 2b6d4011e..ab859ae19 100644 --- a/frontend/src/stories/Final.stories.jsx +++ b/frontend/src/stories/Final.stories.jsx @@ -10,8 +10,8 @@ export default { }, }; -export const Default = { - args: { +function getFinalData(overrides = {}) { + return { score: 100, rank: { text: "Rank", @@ -21,15 +21,15 @@ export const Default = { points: "points", button: { text: "Button", - link: "https://www.google.com", + link: "https://www.example.com", }, logo: { image: "https://via.placeholder.com/150", - link: "https://www.google.com", + link: "https://www.example.com", }, social: { apps: ["facebook", "whatsapp", "twitter", "weibo", "share", "clipboard"], - url: "https://www.google.com", + url: "https://www.example.com", message: "Message", hashtags: ["hashtag"], text: "Text", @@ -46,22 +46,61 @@ export const Default = { button: "Submit", thank_you: "Thank you for your feedback!", contact_body: - '

Please contact us at info@example.com if you have any questions.

', + '

Please contact us at ', }, experiment: { slug: "test", }, participant: "test", - }, - decorators: [ - (Story) => ( -

- - - -
- ), - ], + onNext: () => { alert("Next"); }, + ...overrides, + }; +} + +const getDecorator = (Story) => ( +
+ + + +
+); + +export const Default = { + args: getFinalData(), + decorators: [getDecorator], +}; + +// with relative button.link +export const RelativeButtonLink = { + args: getFinalData({ + button: { + text: "Play again", + link: "/profile", + }, + }), + decorators: [getDecorator], }; + +// with absolute button.link +export const AbsoluteButtonLink = { + args: getFinalData({ + button: { + text: "Button", + link: "https://www.example.com", + }, + }), + decorators: [getDecorator], +}; + +// without button.link +export const NoButtonLink = { + args: getFinalData({ + button: { + text: "Button", + link: "", + }, + }), + decorators: [getDecorator], +}; \ No newline at end of file