Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Experiment form using React #1455

Draft
wants to merge 89 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
ae4081e
feat: integrate Django REST framework and add Experiment API endpoints
drikusroor Dec 20, 2024
2427ea7
feat: add initial React + TypeScript + Vite setup with Tailwind CSS a…
drikusroor Dec 20, 2024
c1e4511
feat: add react-router-dom for routing and update ExperimentForm inte…
drikusroor Dec 20, 2024
ecb82fd
feat: implement experiments overview and fetch hook, update routing f…
drikusroor Dec 20, 2024
c4a7dce
feat: add collapsible sidebar navigation to Experiment forms
drikusroor Dec 20, 2024
fcf0f13
feat: refactor ExperimentForm and ExperimentsOverview to use a new Pa…
drikusroor Dec 20, 2024
3cc25ca
feat: add support for translated content in Experiment model and forms
drikusroor Dec 20, 2024
1a2ceee
feat: add Button component and integrate it into ExperimentForm and E…
drikusroor Dec 20, 2024
4709a0e
feat: add phases management to ExperimentForm with Accordion component
drikusroor Dec 20, 2024
35943bd
feat: prevent form submission on Accordion button click
drikusroor Dec 20, 2024
8382313
feat: preserve existing phase IDs when updating phases in ExperimentForm
drikusroor Dec 20, 2024
09ae97d
feat: add button type to Tabs component for improved accessibility
drikusroor Dec 20, 2024
5b5755b
feat: update delete experiment functionality to use createEntityUrl f…
drikusroor Dec 20, 2024
203d9a7
feat: enhance Tabs component with action buttons for improved functio…
drikusroor Dec 20, 2024
a1ad237
feat: implement tab navigation in ExperimentForm for better content o…
drikusroor Dec 20, 2024
f8061df
feat: add form components (FormField, Input, Select, Textarea) for im…
drikusroor Dec 20, 2024
6abcdaf
feat: refactor TranslatedContentForm to enhance tab navigation and im…
drikusroor Dec 20, 2024
483df8d
feat: adjust sidebar width for improved responsiveness in ExperimentForm
drikusroor Dec 20, 2024
7c987c7
feat: enhance ExperimentForm with phase management features including…
drikusroor Dec 20, 2024
4d52075
feat: update TranslatedContentForm to improve grid responsiveness on …
drikusroor Dec 20, 2024
0f24cf5
feat: update PhaseForm layout to use grid for improved responsiveness
drikusroor Dec 20, 2024
75d2588
feat: enhance Button and ExperimentForm components with unsaved chang…
drikusroor Dec 20, 2024
b4dc184
feat: remove unused Button import from TranslatedContentForm component
drikusroor Dec 20, 2024
34070cd
feat: refactor ExperimentForm, PhaseForm, and TranslatedContentForm t…
drikusroor Dec 20, 2024
ac14c9d
feat: add PhasesPills and LanguagePills components to display phases …
drikusroor Dec 20, 2024
c97ae8b
feat: add Flag component and integrate flags in ExperimentsOverview a…
drikusroor Dec 20, 2024
b163634
feat: add comprehensive list of ISO language codes to constants
drikusroor Dec 20, 2024
cd73f8e
feat: add icon support to Tabs component and update ExperimentForm an…
drikusroor Dec 20, 2024
673e6a3
feat: add experiment icon to navigation in App component
drikusroor Dec 20, 2024
c1f2174
feat: update layout styles in App and Page components for improved re…
drikusroor Dec 20, 2024
e6839dc
refactor: remove unused Experiment type definition in ExperimentsOver…
drikusroor Dec 20, 2024
e82e007
feat: implement block management with API integration and form handling
drikusroor Dec 20, 2024
74ec300
style: improve spacing and background color in ExperimentForm component
drikusroor Dec 20, 2024
047aa89
feat: refactor ExperimentForm to use PhasesForm component for phase m…
drikusroor Dec 20, 2024
543edda
feat: enhance block and phase management with update or create functi…
drikusroor Dec 20, 2024
2874dc9
refactor: remove debug print statement from ExperimentSerializer
drikusroor Dec 20, 2024
ec0583a
feat: add draggable functionality for reordering blocks in PhaseForm …
drikusroor Dec 20, 2024
784ec3a
feat: implement JWT authentication with login functionality and secur…
drikusroor Dec 22, 2024
a16ad6e
fix: add missing key prop to Flag component in ExperimentsOverview
drikusroor Dec 22, 2024
e237b5a
feat: add logout button to navigation in App component
drikusroor Dec 22, 2024
3399ec7
refactor: improve layout and structure of navigation items in App com…
drikusroor Dec 22, 2024
448383e
feat: implement custom useMutation hook for API requests and refactor…
drikusroor Dec 22, 2024
c8a2440
fix: add JWT expiration handling and automatic logout in App component
drikusroor Dec 23, 2024
1161d7a
refactor: remove unused CSS import in App component
drikusroor Dec 23, 2024
c0222ec
chore: update eslint version to 9.17.0 in experiment-form package.json
drikusroor Dec 23, 2024
588ebf8
feat: add route for home page to redirect to ExperimentsOverview
drikusroor Dec 23, 2024
280c1e5
feat: update PhaseForm component to remove block management and add T…
drikusroor Dec 23, 2024
1840e5b
refactor: reorganize JWT settings in base_settings.py for clarity (Fi…
drikusroor Dec 23, 2024
85174ea
feat: add delete functionality to PhaseForm and BlockForm components
drikusroor Dec 23, 2024
7fe3ebd
refactor: remove unused FiTrash2 icon import in PhasesForm component
drikusroor Dec 23, 2024
d5ecbbb
fix: update Timeline component to change inactive icon color from gra…
drikusroor Dec 23, 2024
a284f69
fix: ensure updated phases are correctly indexed after adding a new p…
drikusroor Dec 23, 2024
bb564c5
feat: integrate Zustand for state management in ExperimentForm and re…
drikusroor Dec 23, 2024
e05e1ba
feat: implement patchExperiment function for partial updates in Exper…
drikusroor Dec 23, 2024
0304464
feat: update routing and navigation in ExperimentForm and PhasesForm …
drikusroor Dec 23, 2024
b785a8b
feat: enhance routing and state management in ExperimentForm and Phas…
drikusroor Dec 24, 2024
175da12
feat: refactor phase and block handling in PhasesForm component for i…
drikusroor Dec 30, 2024
cd5f5ad
fix: add optional chaining to prevent errors when accessing phases in…
drikusroor Dec 30, 2024
dee38f1
refactor: remove unused route in PhasesForm component for cleaner code
drikusroor Dec 30, 2024
9cf5ce1
feat: improve add button visibility and styling in Timeline component…
drikusroor Dec 31, 2024
8861ced
feat: improve tab handling and navigation in TranslatedContentForm co…
drikusroor Dec 31, 2024
b7b064e
feat: add wrap prop to Tabs component for improved tab layout handling
drikusroor Dec 31, 2024
2962c40
feat: enable wrap prop in TranslatedContentForm for improved tab layo…
drikusroor Dec 31, 2024
7677223
refactor: remove console.log statements in PhaseForm and TranslatedCo…
drikusroor Dec 31, 2024
cd718ce
refactor: remove jwt prop from Routes in App component for cleaner code
drikusroor Dec 31, 2024
d29c26f
feat: replace anchor tags with Link components for improved routing i…
drikusroor Dec 31, 2024
74c2363
feat: navigate to phases tab on experiment resource load in Experimen…
drikusroor Dec 31, 2024
45f7676
fix: correct navigation index in TranslatedContentForm and ensure act…
drikusroor Dec 31, 2024
8e06a15
fix: handle null phase case in PhaseForm to prevent rendering issues
drikusroor Dec 31, 2024
1d12014
chore: update react and react-dom to version 19 in package.json
drikusroor Dec 31, 2024
255ebe8
feat: implement TranslatedContentForms component for managing multipl…
drikusroor Jan 1, 2025
471ae3d
feat: add toast notifications for experiment save success and error h…
drikusroor Jan 1, 2025
39d90c3
feat: export Toast interface for improved toast notification handling
drikusroor Jan 1, 2025
2b34e47
feat: add Checkbox component for enhanced form functionality
drikusroor Jan 2, 2025
efd5a61
feat: add API endpoint for retrieving playlists and improve section U…
drikusroor Jan 2, 2025
5e1ec1b
feat: update PostgreSQL image to version 16 and refine docker-compose…
drikusroor Jan 7, 2025
7678260
feat: implement JWT management using Zustand for authentication state
drikusroor Jan 7, 2025
1ddb626
feat: add "Add Phase" button to PhasesForm for improved phase management
drikusroor Jan 7, 2025
940ffde
feat: update toast message level to success and enhance styling for i…
drikusroor Jan 7, 2025
4831ab4
feat: add ESLint configuration and necessary dependencies for improve…
drikusroor Jan 7, 2025
62c2437
refactor: remove unused imports and simplify useFetch hook parameters
drikusroor Jan 7, 2025
6bff62b
feat: add MarkdownEditor component and integrate it into TranslatedCo…
drikusroor Jan 7, 2025
8f7066c
feat: integrate MarkdownEditor into TranslatedContentForm and enhance…
drikusroor Jan 7, 2025
ae33214
feat: add BlockTranslatedContent serialization and forms for managing…
drikusroor Jan 7, 2025
e60a6c8
fix: make toast messages stack
drikusroor Jan 7, 2025
aefb343
feat: add PlaylistSerializer and integrate playlists into BlockSerial…
drikusroor Jan 7, 2025
381bffa
feat: add playlists support to BlockSerializer and BlockForm; impleme…
drikusroor Jan 8, 2025
8564426
feat: add error handling for missing experiment and update tab label …
drikusroor Jan 8, 2025
ad422e8
feat: add keyboard shortcut for saving experiments and update save bu…
drikusroor Jan 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/aml/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
from corsheaders.defaults import default_headers
import sentry_sdk

# JWT Settings
from datetime import timedelta

# Workaround for deprecated ugettext_lazy in django-inline-actions
import django
from django.utils.translation import gettext_lazy
Expand All @@ -41,6 +44,7 @@
# Application definition

INSTALLED_APPS = [
"rest_framework",
"admin_interface",
"modeltranslation", # Must be before django.contrib.admin
"django.contrib.admin",
Expand Down Expand Up @@ -208,3 +212,13 @@
MARKUP_SETTINGS = {"markdown": {"safe_mode": False}}

SUBPATH = os.getenv("AML_SUBPATH", "")

# REST framework settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",),
}

SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
31 changes: 19 additions & 12 deletions backend/aml/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""

from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static

from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)


# Admin strings
admin.site.site_header = "AML Admin"
Expand All @@ -26,31 +32,32 @@

# Urls patterns
urlpatterns = [
path('experiment/', include('experiment.urls')),
path('question/', include('question.urls')),
path('participant/', include('participant.urls')),
path('result/', include('result.urls')),
path('section/', include('section.urls')),
path('session/', include('session.urls')),
path('theme/', include('theme.urls')),
path('admin_interface/', include('admin_interface.urls')),
path('admin/', admin.site.urls),

path("experiment/", include("experiment.urls")),
path("question/", include("question.urls")),
path("participant/", include("participant.urls")),
path("result/", include("result.urls")),
path("section/", include("section.urls")),
path("session/", include("session.urls")),
path("theme/", include("theme.urls")),
path("admin_interface/", include("admin_interface.urls")),
path("admin/", admin.site.urls),
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
Comment on lines +44 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used by the experiment form react application to obtain a JWT token that is used to authenticate the user in Rest API requests

# Sentry debug (uncomment to test Sentry)
# path('sentry-debug/', lambda request: 1 / 0),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# ^ The static helper function only works in debug mode
# (https://docs.djangoproject.com/en/3.0/howto/static-files/)


# Prefix all URLS with /server if AML_SUBPATH is set
if settings.SUBPATH:
urlpatterns = [path('server/', include(urlpatterns))]
urlpatterns = [path("server/", include(urlpatterns))]

# Debug toolbar
if settings.DEBUG and not settings.TESTING:
import debug_toolbar

urlpatterns += [
path("__debug__/", include(debug_toolbar.urls)),
]
185 changes: 184 additions & 1 deletion backend/experiment/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,197 @@

from django_markup.markup import formatter
from django.utils.translation import activate, get_language
from rest_framework import serializers

from experiment.actions.consent import Consent
from image.serializers import serialize_image
from participant.models import Participant
from result.models import Result
from session.models import Session
from theme.serializers import serialize_theme
from .models import Block, Experiment, Phase, SocialMediaConfig
from .models import Block, Experiment, Phase, SocialMediaConfig, ExperimentTranslatedContent, BlockTranslatedContent
from section.models import Playlist


class ExperimentTranslatedContentSerializer(serializers.ModelSerializer):
class Meta:
model = ExperimentTranslatedContent
fields = ["id", "index", "language", "name", "description", "about_content", "social_media_message"]


class BlockTranslatedContentSerializer(serializers.ModelSerializer):
class Meta:
model = BlockTranslatedContent
fields = ["language", "name", "description"]


class PlaylistSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)

class Meta:
model = Playlist
fields = ["id", "name"]


class BlockSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
translated_contents = BlockTranslatedContentSerializer(many=True, required=False, read_only=False)
playlists = PlaylistSerializer(many=True, required=False)

class Meta:
model = Block
fields = [
"id",
"index",
"slug",
"rounds",
"bonus_points",
"rules",
"translated_contents",
"playlists", # many to many field
]
extra_kwargs = {
"slug": {"validators": []},
}

def create(self, validated_data):
translated_contents_data = validated_data.pop("translated_contents", [])
playlists_data = validated_data.pop("playlists", [])
block = Block.objects.create(**validated_data)

for content_data in translated_contents_data:
BlockTranslatedContent.objects.create(block=block, **content_data)

for playlist_data in playlists_data:
playlist = Playlist.objects.get(pk=playlist_data["id"])
block.playlists.add(playlist)

return block

def update(self, instance, validated_data):
translated_contents_data = validated_data.pop("translated_contents", [])
playlists_data = validated_data.pop("playlists", [])

for attr, value in validated_data.items():
setattr(instance, attr, value)

BlockTranslatedContent.objects.filter(block=instance).delete()
for content_data in translated_contents_data:
BlockTranslatedContent.objects.create(block=instance, **content_data)

existing_playlist_ids = set()
for playlist_data in playlists_data:
playlist_id = playlist_data.get("id")
if playlist_id:
playlist = Playlist.objects.get(pk=playlist_id)
instance.playlists.add(playlist)
existing_playlist_ids.add(playlist.id)

# Delete removed playlists
instance.playlists.exclude(id__in=existing_playlist_ids).delete()

instance.save()

return instance


class PhaseSerializer(serializers.ModelSerializer):
blocks = BlockSerializer(many=True, required=False)

class Meta:
model = Phase
fields = ["id", "index", "dashboard", "randomize", "blocks"]


class ExperimentSerializer(serializers.ModelSerializer):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the weak part about this setup (for now) I think. It seems to be a bit convoluted but I do think it should be able to simplify it. It's like this right now because I had to deal with different scenarios like many to many relationships, or just FKs, do the related objects have PKs already or not, and so on. Maybe a good step is just to create and save an empty experiment before you get to the form. Then at least the experiment already exists and we can remove the create method in here.

translated_content = ExperimentTranslatedContentSerializer(many=True, required=False)
phases = PhaseSerializer(many=True, required=False)

class Meta:
model = Experiment
fields = ["id", "slug", "active", "translated_content", "phases"]

def create(self, validated_data):
translated_content_data = validated_data.pop("translated_content", [])
phases_data = validated_data.pop("phases", [])
experiment = Experiment.objects.create(**validated_data)

for content_data in translated_content_data:
ExperimentTranslatedContent.objects.create(experiment=experiment, **content_data)

for phase_data in phases_data:
blocks_data = phase_data.pop("blocks", [])
phase = Phase.objects.create(experiment=experiment, **phase_data)

for block_data in blocks_data:
Block.objects.create(phase=phase, **block_data)

return experiment

def update(self, instance, validated_data):
translated_content_data = validated_data.pop("translated_content", [])
phases_data = validated_data.pop("phases", [])

# Update experiment fields
instance.slug = validated_data.get("slug", instance.slug)
instance.active = validated_data.get("active", instance.active)
instance.save()

# Update translated content
if translated_content_data is not None:
existing_content_ids = set()

# Update or create translated content
for content_data in translated_content_data:
content_id = content_data.get("id")
if content_id:
content, _ = ExperimentTranslatedContent.objects.update_or_create(
id=content_id, experiment=instance, defaults=content_data
)
else:
content = ExperimentTranslatedContent.objects.create(experiment=instance, **content_data)
existing_content_ids.add(content.id)

# Delete removed content
instance.translated_content.exclude(id__in=existing_content_ids).delete()

# Update phases
if phases_data is not None:
existing_phase_ids = set()

# Update or create phases
for phase_data in phases_data:
blocks_data = phase_data.pop("blocks", [])
phase_id = phase_data.get("id")

if phase_id:
phase, _ = Phase.objects.update_or_create(id=phase_id, experiment=instance, defaults=phase_data)
else:
phase = Phase.objects.create(experiment=instance, **phase_data)
existing_phase_ids.add(phase.id)

# Handle blocks for this phase
existing_block_ids = set()

for block_data in blocks_data:
block_id = block_data.get("id")

if block_id:
block_instance = Block.objects.get(pk=block_id)
block_serializer = BlockSerializer(block_instance, data=block_data, partial=True)
else:
block_serializer = BlockSerializer(data=block_data, partial=True)
block_serializer.is_valid(raise_exception=True)
block = block_serializer.save(phase=phase)
existing_block_ids.add(block.id)

# Delete removed blocks
phase.blocks.exclude(id__in=existing_block_ids).delete()

# Delete removed phases
instance.phases.exclude(id__in=existing_phase_ids).delete()

return instance


def serialize_actions(actions):
Expand Down
18 changes: 18 additions & 0 deletions backend/experiment/templates/form/experiment-form/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions backend/experiment/templates/form/experiment-form/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
30 changes: 30 additions & 0 deletions backend/experiment/templates/form/experiment-form/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
Binary file not shown.
Loading
Loading