Skip to content

Commit

Permalink
Merge pull request #753 from Amsterdam-Music-Lab/fix/session-creation
Browse files Browse the repository at this point in the history
Fix/session creation
  • Loading branch information
BeritJanssen authored Feb 14, 2024
2 parents 6b0e7b5 + 47ba959 commit b1c398d
Show file tree
Hide file tree
Showing 21 changed files with 293 additions and 151 deletions.
47 changes: 47 additions & 0 deletions backend/session/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from django.test import TestCase

from experiment.models import Experiment
from participant.models import Participant
from section.models import Playlist
from session.models import Session


class SessionViewsTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.participant = Participant.objects.create(unique_hash=42)
cls.playlist1 = Playlist.objects.create(name='First Playlist')
cls.playlist2 = Playlist.objects.create(name='Second Playlist')
cls.experiment = Experiment.objects.create(
name='TestViews',
slug='testviews'
)
cls.experiment.playlists.add(
cls.playlist1, cls.playlist2
)

def setUp(self):
session = self.client.session
session['participant_id'] = self.participant.id
session.save()

def test_create_with_playlist(self):
request = {
"experiment_id": self.experiment.id,
"playlist_id": self.playlist2.id
}
self.client.post('/session/create/', request)
new_session = Session.objects.get(
experiment=self.experiment, participant=self.participant)
assert new_session
assert new_session.playlist == self.playlist2

def test_create_without_playlist(self):
request = {
"experiment_id": self.experiment.id
}
self.client.post('/session/create/', request)
new_session = Session.objects.get(
experiment=self.experiment, participant=self.participant)
assert new_session
assert new_session.playlist == self.playlist1
7 changes: 3 additions & 4 deletions backend/session/urls.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from django.urls import path
from .views import create_session, continue_session, next_round, finalize_session, register_playlist
from .views import create_session, continue_session, next_round, finalize_session


app_name='session'

urlpatterns = [
path('create/',
create_session, name='session_create'),
path('<int:session_id>/next_round/',
next_round, name='session_next_round'),
path('<int:session_id>/register_playlist/',
register_playlist, name='register_playlist'),
next_round, name='session_next_round'),
path('continue/<int:session_id>',
continue_session, name='continue_session'),
path('<int:session_id>/finalize/', finalize_session)
Expand Down
28 changes: 8 additions & 20 deletions backend/session/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ def create_session(request):
# Create new session
session = Session(experiment=experiment, participant=participant)

if experiment.playlists.count() >= 1:
if request.POST.get("playlist_id"):
try:
playlist = Playlist.objects.get(
pk=request.POST.get("playlist_id"), experiment__id=session.experiment.id)
session.playlist = playlist
except:
raise Http404("Playlist does not exist")
elif experiment.playlists.count() >= 1:
# register first playlist
session.playlist = experiment.playlists.first()

Expand All @@ -41,25 +48,6 @@ def create_session(request):
return JsonResponse({'session': {'id': session.id}})


@require_POST
def register_playlist(request, session_id):
# load playlist from request
playlist_id = request.POST.get("playlist_id")
if not playlist_id:
return HttpResponseBadRequest("playlist_id not defined")
participant = get_participant(request)
session = get_object_or_404(Session,
pk=session_id, participant__id=participant.id)
try:
playlist = Playlist.objects.get(
pk=playlist_id, experiment__id=session.experiment.id)
session.playlist = playlist
session.save()
return JsonResponse({'success': True})
except Playlist.DoesNotExist:
raise Http404("Playlist does not exist")


def continue_session(request, session_id):
""" given a session_id, continue where we left off """

Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"@storybook/react-webpack5": "7.6.6",
"@storybook/testing-library": "0.2.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.1",
"babel-plugin-named-exports-order": "0.0.2",
"coverage-badges-cli": "^1.2.5",
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,17 @@ export const createConsent = async ({ experiment, participant }) => {
};

// Create a new session for given experiment
export const createSession = async ( {experiment, participant} ) => {
export const createSession = async ( {experiment, participant, playlist} ) => {
try {
const response = await axios.post(
API_BASE_URL + URLS.session.create,
qs.stringify({
experiment_id: experiment.id,
playlist_id: playlist.current,
csrfmiddlewaretoken: participant.csrf_token,
})
);
return response.data;
return response.data.session;
} catch (err) {
console.error(err);
return null;
Expand Down
118 changes: 61 additions & 57 deletions frontend/src/components/Experiment/Experiment.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import { withRouter } from "react-router-dom";
import classNames from "classnames";
Expand All @@ -19,7 +19,6 @@ import Info from "../Info/Info";
import FloatingActionButton from "components/FloatingActionButton/FloatingActionButton";
import UserFeedback from "components/UserFeedback/UserFeedback";


// Experiment handles the main experiment flow:
// - Loads the experiment and participant
// - Renders the view based on the state that is provided by the server
Expand All @@ -28,95 +27,106 @@ import UserFeedback from "components/UserFeedback/UserFeedback";
// Empty URL parameter "participant_id" is the same as no URL parameter at all
const Experiment = ({ match }) => {
const startState = { view: "LOADING" };
// Stores
const setError = useErrorStore(state => state.setError);
const participant = useParticipantStore((state) => state.participant);
const setSession = useSessionStore((state) => state.setSession);
const session = useSessionStore((state) => state.session);

// Current experiment state
const [state, setState] = useState(startState);
const [playlist, setPlaylist] = useState(null);
const [actions, setActions] = useState([]);
const [state, setState] = useState(startState);
const playlist = useRef(null);

// API hooks
const [experiment, loadingExperiment] = useExperiment(match.params.slug);

const loadingText = experiment ? experiment.loading_text : "";
const className = experiment ? experiment.class_name : "";

// Load state, set random key
const loadState = useCallback((state) => {
// set random key before setting state
// this will assure that `state` will be recognized as an updated object
const updateState = useCallback((state) => {
if (!state) return;
state.key = Math.random();
setState(state);
}, []);

// Create error view
const setError = useErrorStore(state => state.setError);

const updateActions = useCallback((currentActions) => {
let newActions = currentActions;
const newState = newActions.shift();
loadState(newState);
const newActions = currentActions;
setActions(newActions);
}, [loadState, setActions]);
const newState = newActions.shift();
updateState(newState);
}, [updateState]);

useEffect(() => {
if (!experiment || !participant ) {
return;
const checkSession = useCallback(async () => {
if (session) {
return session;
}
createSession({
experiment,
participant
}).then(data => {
setSession(data.session);
}).catch(err => {
try {
const newSession = await createSession({experiment, participant, playlist})
setSession(newSession);
return newSession;
}
catch(err) {
setError(`Could not create a session: ${err}`)
};
}, [experiment, participant, playlist, setError, setSession])

const continueToNextRound = useCallback(async() => {
const thisSession = await checkSession();
// Try to get next_round data from server
const round = await getNextRound({
session: thisSession
});
}, [experiment, participant, setError, setSession])
if (round) {
updateActions(round.next_round);
} else {
setError(
"An error occured while loading the data, please try to reload the page. (Error: next_round data unavailable)"
);
setState(undefined);
}
}, [checkSession, updateActions, setError, setState])

// trigger next action from next_round array, or call session/next_round
const onNext = async (doBreak) => {
if (!doBreak && actions.length) {
updateActions(actions);
} else {
continueToNextRound();
}
};

// Start first_round when experiment and partipant have been loaded
useEffect(() => {
// Check if done loading
if (!loadingExperiment && participant) {
// Loading succeeded
if (experiment) {
updateActions(experiment.next_round);
if (experiment.next_round.length) {
const firstActions = [ ...experiment.next_round ];
updateActions(firstActions);
} else {
continueToNextRound();
}
} else {
// Loading error
setError("Could not load experiment");
}
}
}, [
continueToNextRound,
experiment,
loadingExperiment,
participant,
setError,
updateActions
]);

// trigger next action from next_round array, or call session/next_round
const onNext = async (doBreak) => {
if (!doBreak && actions.length) {
updateActions(actions);
} else {
// Try to get next_round data from server
const round = await getNextRound({
session
});
if (round) {
updateActions(round.next_round);
} else {
setError(
"An error occured while loading the data, please try to reload the page. (Error: next_round data unavailable)"
);
}
}
};

const onResult = useResultHandler({
session,
participant,
loadState,
onNext,
state,
});
Expand All @@ -127,12 +137,10 @@ const Experiment = ({ match }) => {
const attrs = {
experiment,
participant,
loadState,
playlist,
loadingText,
setPlaylist,
onResult,
onNext,
playlist,
...state,
};

Expand Down Expand Up @@ -184,12 +192,8 @@ const Experiment = ({ match }) => {
setError('No valid state');
}

let key = state.view;
const view = state.view;

// Force view refresh for consecutive questions
if (state.view === "QUESTION") {
key = state.question.key;
}
return (
<TransitionGroup
className={classNames(
Expand All @@ -201,25 +205,25 @@ const Experiment = ({ match }) => {
data-testid="experiment-wrapper"
>
<CSSTransition
key={key}
key={view}
timeout={{ enter: 300, exit: 0 }}
classNames={"transition"}
unmountOnExit
>
{(!loadingExperiment && experiment) || key === "ERROR" ? (
{(!loadingExperiment && experiment) || view === "ERROR" ? (
<DefaultPage
title={state.title}
logoClickConfirm={
["FINAL", "ERROR", "TOONTJEHOGER"].includes(key) ||
["FINAL", "ERROR", "TOONTJEHOGER"].includes(view) ||
// Info pages at end of experiment
(key === "INFO" &&
(view === "INFO" &&
(!state.next_round || !state.next_round.length))
? null
: "Are you sure you want to stop this experiment?"
}
className={className}
>
{render(state.view)}
{render(view)}

{experiment?.feedback_info?.show_float_button && (
<FloatingActionButton>
Expand Down
Loading

0 comments on commit b1c398d

Please sign in to comment.