-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: develop
Are you sure you want to change the base?
Changes from all commits
ae4081e
2427ea7
c1e4511
ecb82fd
c4a7dce
fcf0f13
3cc25ca
1a2ceee
4709a0e
35943bd
8382313
09ae97d
5b5755b
203d9a7
a1ad237
f8061df
6abcdaf
483df8d
7c987c7
4d52075
0f24cf5
75d2588
b4dc184
34070cd
ac14c9d
c97ae8b
b163634
cd73f8e
673e6a3
c1f2174
e6839dc
e82e007
74ec300
047aa89
543edda
2874dc9
ec0583a
784ec3a
a16ad6e
e237b5a
3399ec7
448383e
c8a2440
1161d7a
c0222ec
588ebf8
280c1e5
1840e5b
85174ea
7fe3ebd
d5ecbbb
a284f69
bb564c5
e05e1ba
0304464
b785a8b
175da12
cd5f5ad
dee38f1
9cf5ce1
8861ced
b7b064e
2962c40
7677223
cd718ce
d29c26f
74c2363
45f7676
8e06a15
1d12014
255ebe8
471ae3d
39d90c3
2b34e47
efd5a61
5e1ec1b
7678260
1ddb626
940ffde
4831ab4
62c2437
6bff62b
8f7066c
ae33214
e60a6c8
aefb343
381bffa
8564426
ad422e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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): | ||
|
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 }, | ||
], | ||
}, | ||
} |
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? |
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 |
There was a problem hiding this comment.
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