Skip to content

Commit

Permalink
Refactor: Componentify participant condition, loader container, and c…
Browse files Browse the repository at this point in the history
…leanup several imports (#917)

* fix: Pass participant id to get experiment collection

* fix(lint): Fix formatting in App.jsx

* test: Update ExperimentCollectionDashboard.test.tsx with new tests and fix linting issues

* refactor: Migrate Zustand store to Typescript and add optional Sentry error capture

* type: Add Participant interface

* revert: Use existing fetch participant functionality and make sure participant is loaded before fetching the experiment collection

* refactor: Convert App.jsx & config.js to .tsx and .ts files

* refactor: Update Participant "current" view to include participant_id_url field

* refactor: Use participantIdUrl instead of participantId to link / redirect to experiments with pre-existing participant_id(_url)

* refactor: Add LoaderContainer and ConditionalRender components

* refactor: Update CongoSameDiff to get participant's group variant based on participant's id or random number

* fix: Handle missing participant / participant id in experiment collection

* test: Test link to experiment with participant id url param

* refactor: Remove unused import in ExperimentCollection.tsx

* fix: Fix linting warnings
  • Loading branch information
drikusroor authored Apr 29, 2024
1 parent cd6cf9f commit c5422eb
Show file tree
Hide file tree
Showing 32 changed files with 322 additions and 170 deletions.
7 changes: 4 additions & 3 deletions backend/experiment/rules/congosamediff.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

import random
import re
import math
import string
Expand Down Expand Up @@ -112,10 +113,10 @@ def next_round(self, session: Session):
groups_amount = session.playlist.section_set.values('group').distinct().count()
variants_amount = real_trial_variants.count()

# get the participant's group variant
participant_id = session.participant.participant_id_url
# get the participant's group variant based on the participant's id # else default to random number between 1 and variants_amount
participant_id = int(session.participant.participant_id_url) if session.participant.participant_id_url else random.randint(1, variants_amount * 2)
participant_group_variant = self.get_participant_group_variant(
int(participant_id),
participant_id,
group_number,
groups_amount,
variants_amount
Expand Down
2 changes: 1 addition & 1 deletion backend/participant/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def export_admin(self):
"participant_id_url": self.participant_id_url,
"profile": self.profile_object()
}

def export_profiles(self):
# export participant profile result objects
return self.result_set.all()
Expand Down
1 change: 1 addition & 0 deletions backend/participant/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def current(request):
'id': participant.id,
'hash': participant.unique_hash,
'csrf_token': get_token(request),
'participant_id_url': participant.participant_id_url,
'country': participant.country_code
}, json_dumps_params={'indent': 4})
return response
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/API.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { API_BASE_URL } from "./config";
import { API_BASE_URL } from "@/config";
import useGet from "./util/useGet";
import axios from "axios";
import qs from "qs";
Expand All @@ -15,7 +15,7 @@ export const URLS = {
feedback: (slug) => "/experiment/" + slug + "/feedback/",
},
experiment_collection: {
get: (slug) => "/experiment/collection/" + slug + "/"
get: (slug) => `/experiment/collection/${slug}/`
},
participant: {
current: "/participant/",
Expand Down
100 changes: 0 additions & 100 deletions frontend/src/components/App/App.jsx

This file was deleted.

100 changes: 100 additions & 0 deletions frontend/src/components/App/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useEffect } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from "react-router-dom";
import axios from "axios";

import { API_BASE_URL, EXPERIMENT_SLUG, URLS } from "@/config";
import { URLS as API_URLS } from "../../API";
import useBoundStore from "../../util/stores";
import Experiment from "../Experiment/Experiment";
import ExperimentCollection from "../ExperimentCollection/ExperimentCollection";
import LoaderContainer from "../LoaderContainer/LoaderContainer";
import ConditionalRender from "../ConditionalRender/ConditionalRender";
import Profile from "../Profile/Profile";
import Reload from "../Reload/Reload";
import StoreProfile from "../StoreProfile/StoreProfile";
import useDisableRightClickOnTouchDevices from "../../hooks/useDisableRightClickOnTouchDevices";


// App is the root component of our application
const App = () => {
const error = useBoundStore(state => state.error);
const setError = useBoundStore(state => state.setError);
const participant = useBoundStore((state) => state.participant);
const setParticipant = useBoundStore((state) => state.setParticipant);
const setParticipantLoading = useBoundStore((state) => state.setParticipantLoading);
const queryParams = window.location.search;

useDisableRightClickOnTouchDevices();

useEffect(() => {
const urlParams = new URLSearchParams(queryParams);
const participantId = urlParams.get('participant_id');
let participantQueryParams = '';
if (participantId) {
participantQueryParams = `?participant_id=${participantId}`;
}
try {
axios.get(API_BASE_URL + API_URLS.participant.current + participantQueryParams).then(response => {
setParticipant(response.data);
});
} catch (err) {
console.error(err);
setError('Could not load participant', err);
} finally {
setParticipantLoading(false);
}
}, [setError, queryParams, setParticipant])

if (error) {
return <p className="aha__error">Error: {error}</p>;
}

return (
<Router className="aha__app">
<ConditionalRender condition={!!participant} fallback={<LoaderContainer />}>
<Switch>
{/* Request reload for given participant */}
<Route path={URLS.reloadParticipant}>
<Reload />
</Route>

{/* Default experiment */}
<Route path="/" exact>
<Redirect
to={URLS.experiment.replace(":slug", EXPERIMENT_SLUG)}
/>
</Route>

{/* Profile */}
<Route path={URLS.profile} exact>
<Profile slug={EXPERIMENT_SLUG} />
</Route>

{/* Experiment Collection */}
<Route path={URLS.experimentCollection} component={ExperimentCollection} />

{/* Experiment */}
<Route path={URLS.experiment} component={Experiment} />

<Route path={URLS.session} />

{/* Store profile */}
<Route
path={URLS.storeProfile}
exact
component={StoreProfile}
/>
</Switch>
</ConditionalRender>


</Router >
);
};

export default App;
2 changes: 1 addition & 1 deletion frontend/src/components/AppBar/AppBar.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { API_BASE_URL, URLS, LOGO_URL, LOGO_TITLE } from "../../config";
import { API_BASE_URL, URLS, LOGO_URL, LOGO_TITLE } from "@/config";
import { Link } from "react-router-dom";
import useBoundStore from "@/util/stores";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ConditionalRender from "./ConditionalRender";
import { render, screen } from '@testing-library/react';
import { it, expect, describe } from "vitest";

describe("ConditionalRender Component", () => {
it("should render children when condition is true", () => {
render(
<ConditionalRender condition={true} fallback={<div>fallback</div>}>
<div>children</div>
</ConditionalRender>
);

expect(document.body.contains(screen.getByText("children"))).toBe(true);
expect(document.body.contains(screen.queryByText("fallback"))).toBe(false);
});

it("should render fallback when condition is false", () => {
render(
<ConditionalRender condition={false} fallback={<div>fallback</div>}>
<div>children</div>
</ConditionalRender>
);

expect(document.body.contains(screen.getByText("fallback"))).toBe(true);
expect(document.body.contains(screen.queryByText("children"))).toBe(false);
});

it("should render nothing when fallback is not provided and condition is false", () => {
const { container } = render(
<ConditionalRender condition={false}>
<div>children</div>
</ConditionalRender>
);

expect(container.firstChild).toBeNull();
});
});
17 changes: 17 additions & 0 deletions frontend/src/components/ConditionalRender/ConditionalRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';

interface ConditionalRenderProps {
condition: boolean;
children?: React.ReactNode;
fallback?: React.ReactNode;
}

const ConditionalRender = ({ condition, children, fallback }: ConditionalRenderProps) => {
if (condition) {
return children;
}

return fallback || null;
};

export default ConditionalRender;
2 changes: 1 addition & 1 deletion frontend/src/components/Consent/Consent.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { saveAs } from 'file-saver';

import { URLS } from "../../config";
import { URLS } from "@/config";
import Button from "../Button/Button";
import Loading from "../Loading/Loading";
import { createConsent, useConsent } from "../../API";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Experiment/Experiment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const Experiment = ({ match }) => {
return newSession;
}
catch (err) {
setError(`Could not create a session: ${err}`)
setError(`Could not create a session: ${err}`, err)
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,30 @@ import {
Switch
} from "react-router-dom";

import useBoundStore from "../../util/stores";
import { useExperimentCollection } from "../../API";
import Consent from "../Consent/Consent";
import DefaultPage from "../Page/DefaultPage";
import { useExperimentCollection } from "@/API.js";
import Loading from "../Loading/Loading";
import ExperimentCollectionAbout from "./ExperimentCollectionAbout/ExperimentCollectionAbout";
import ExperimentCollectionDashboard from "./ExperimentCollectionDashboard/ExperimentCollectionDashboard";
import { URLS } from "../../config";
import { URLS } from "@/config";
import IExperimentCollection from "@/types/ExperimentCollection";
import { useBoundStore } from "@/util/stores";
import { Participant } from "@/types/Participant";

interface RouteParams {
slug: string
}

interface ExperimentCollectionProps extends RouteComponentProps<RouteParams> {
participant: Participant
}

const ExperimentCollection = ({ match }: ExperimentCollectionProps) => {
const [experimentCollection, loadingExperimentCollection] = useExperimentCollection(match.params.slug) as [IExperimentCollection, boolean];
const [hasShownConsent, setHasShownConsent] = useState(false);
const participant = useBoundStore((state) => state.participant);
const participantIdUrl = participant?.participant_id_url;
const nextExperiment = experimentCollection?.next_experiment;
const displayDashboard = experimentCollection?.dashboard.length;
const showConsent = experimentCollection?.consent;
Expand All @@ -35,6 +38,8 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => {
setHasShownConsent(true);
}

const getExperimentHref = (slug: string) => `/${slug}${participantIdUrl ? `?participant_id=${participantIdUrl}` : ""}`;

if (loadingExperimentCollection) {
return (
<div className="loader-container">
Expand All @@ -59,14 +64,14 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => {
}

if (!displayDashboard && nextExperiment) {
return <Redirect to={"/" + nextExperiment.slug} />;
return <Redirect to={getExperimentHref(nextExperiment.slug)} />
}

return (
<div className="aha__collection">
<Switch>
<Route path={URLS.experimentCollectionAbout} component={() => <ExperimentCollectionAbout content={experimentCollection?.about_content} slug={experimentCollection.slug} />} />
<Route path={URLS.experimentCollection} exact component={() => <ExperimentCollectionDashboard experimentCollection={experimentCollection} />} />
<Route path={URLS.experimentCollection} exact component={() => <ExperimentCollectionDashboard experimentCollection={experimentCollection} participantIdUrl={participantIdUrl} />} />
</Switch>
</div>
)
Expand Down
Loading

0 comments on commit c5422eb

Please sign in to comment.