From 37c0a79f20ec339103f9049a18a76eeced2de9a6 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 7 Aug 2025 15:12:44 +0000 Subject: [PATCH 01/32] update Renatos push to main branch --- .pre-commit-config.yaml | 6 +- docs/events/2025-08-06-2nd-LEA-hackathon.md | 63 ++++++++++++++++++ .../2025-12-01-4th-German-Biohackathon.md | 3 + docs/events/index.md | 5 ++ ...tform-Empowering_Trainers_and_Trainees.pdf | Bin 0 -> 24710 bytes .../2025-12-01-4th-German-Biohackathon.pdf | Bin 0 -> 27478 bytes docs/in_progress.md | 3 +- mkdocs.yml | 2 + 8 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 docs/events/2025-08-06-2nd-LEA-hackathon.md create mode 100644 docs/events/2025-12-01-4th-German-Biohackathon.md create mode 100644 docs/events/index.md create mode 100644 docs/events/static/2024-12-09-BiohackathonGermany2024-TraMa-Training_Management_Platform-Empowering_Trainers_and_Trainees.pdf create mode 100644 docs/events/static/2025-12-01-4th-German-Biohackathon.pdf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d9cbd5..8899efc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: args: - --py39-plus - repo: https://github.com/jackdewinter/pymarkdown - rev: v0.9.25 + rev: v0.9.31 hooks: - id: pymarkdown args: @@ -85,6 +85,10 @@ repos: # MD033 - Inline HTML # MD041 - First line in file should be a top level header - --disable-rules=md013,md033,md041 + # The $#4 syntax is how one should specify integers in command-line for pymarkdown + # See https://application-properties.readthedocs.io/en/latest/command-line/#configuration-item-types + - --set + - 'plugins.md007.indent=$#4' - scan - repo: local hooks: diff --git a/docs/events/2025-08-06-2nd-LEA-hackathon.md b/docs/events/2025-08-06-2nd-LEA-hackathon.md new file mode 100644 index 0000000..fee8d5a --- /dev/null +++ b/docs/events/2025-08-06-2nd-LEA-hackathon.md @@ -0,0 +1,63 @@ +# 2nd Internal Hackathon - Apr 6 2025 + +## Morning / Afternoon + +- Overview of progress in last weeks (Adeel, Franziska) +- Status of task execution backends (Harshita) +- Status of waiting list, requirements and UI considerations (Kristen) +- Status of permissions and overview of options (Renato) +- Status of Course vs CourseSession vs (course) Versions/Flavours - Sweet spot to meet needs of different organisations (Nina, Franziska) +- Review of requirement and any other topics (AOB) (Nina) +- Reflection / Lunch break +- High-level discussion aiming at decisions (Everyone) + +## Decisions & Discussions + +### Discussions + +- "Organisation" for different collaborators means different entities from the hierarchy. + - SciLifeLab would host a LEA instance and have NBIS (a platform) as an organisation + - EMBL would host a LEA instance and have "EMBL Heidelberg", "EMBL Rome", ... as organisations. +- Flexibility to modify Questions and QuestionSets can have implications in long-term data collection. + - Changing questions will inviabilize aggregating data over the years. + - :question: Should we add the possitiblity to have Questions in QuestionSets to not be removable? + - Use-case is to avoid cherry-picking questions from certain sets. Encouraging the creation of new QuestionSets fitting the course need. + - :warning: To be considered later. + - Possibility to have a drop-box-like widget to upload files as second step to a registration. Preference to have this instead of specific questions asking for single-file uploads. + - For the drop-box, the backend would only create a folder for the participant+event and not need to track the files. Any content would be assumed to be part of the registration. +- Course flavours (see course discussion document) + - We would like to implement this feature but how we would represent the branching from another Course needs a clearer proposal + +### Decisions + +- Adding a URL field to the Locations DB model +- Add frontend option to allow selecting more than one set of Questions both when defining a QuestionSet and Questions in a course +- Create QuestionSets that are mandatory/core for each organisation. Always present in all Courses +- Required questions for each organisation would **not** be enforced by LEA. The organisation would be responsible for overseeing and having a process to ensure questions are present. +- Attempt to use django-tasks for some of our tasks and consider changing if we run into issues or we can't meet our needs. Priority order is [django-tasks](https://github.com/RealOrangeOne/django-tasks) [(DEP-0014)](https://github.com/django/deps/blob/main/accepted/0014-background-workers.rst), [django-q2](https://django-q2.readthedocs.io), [django-rq](https://github.com/rq/django-rq) and [celery](https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html) +- On the type of course registration format (First-come-first-served, with evaluation, ...) we do not allow this to be changed once a Session is open +- Design and style considerations + - SCSS files using variables that we can customize for themeing. Agreeing on a few basic colors (hex codes) for these variables + - Admin and non-admin pages having slightly different themes to distinguish pages + - Design mockups to use PrimeVue widgets for a closer approximation of the final "look". + - Design feedback will be done in the PowerPoint where visuals are presented. Changes are implemented if simple enough and brought to the weekly meetings if additional discussion or review by others is desired. +- Permissions + - To remove one of the "Organisation" level roles. Pick one and adjust permissions of the remaining one as it becomes clearer. + - Allow creation of new roles with a big red warning about the caveats (updating permissions with new releases of LEA, etc...) + - Don't allow customizing pre-set roles + - Permissions (i.e. rows in table) would reflect CRUD actions on endpoints. + - CRUD actions translate to HTTP requests: Create -> POST, View -> GET, Delete -> DELETE, Update -> PUT + - We need to be cautious to include Organisation, Course and Session in the URLs so permissions are granted to the endpoint URL. E.g. `/api////register`. + - Permissions are then a combination of a CRUD verb and an API endpoint + - We need a "Participant" role + - We need a "No role" column in the permission table but this doesn't need to be role in the system. No role is "No role" + - To provide 2 permission templates (taking example from SciLifeLab and EMBL). Permissions are selected when an organisation is created. +- Hierarchy + - Rename Organisation to Unit. + - Course > CourseSession + - Implement a semi-rigid hierarchy between Course and CourseSession with two options, configurable at the Organisation level: + - Allow all fields to be editable both at the Session level (SciLifeLab use-case) + - Hide fields that directly inherit from Course (EMBL use-case) + - Ability to edit Course information (e.g. correct typos), but other discourage edits at Course level, and encourage creating a Flavour (aka Version) or an entirely new Course + - Agreed to allow templating a Session from both existing Courses and existing Sessions. (This will not require backend changes, only a UI redesign and frontend "magic") +- MVP should include the features discussed today. Possibly not some of the flexible discussions (e.g. QuestionSets) but all the key components to have the system work with role separation. diff --git a/docs/events/2025-12-01-4th-German-Biohackathon.md b/docs/events/2025-12-01-4th-German-Biohackathon.md new file mode 100644 index 0000000..eb76014 --- /dev/null +++ b/docs/events/2025-12-01-4th-German-Biohackathon.md @@ -0,0 +1,3 @@ +# 4th German Biohackathon + +## Action list diff --git a/docs/events/index.md b/docs/events/index.md new file mode 100644 index 0000000..78a6149 --- /dev/null +++ b/docs/events/index.md @@ -0,0 +1,5 @@ +# Events + +- [4th German BioHackathon (Dec 1 2025)](2025-12-01-4th-German-Biohackathon.md) - [Project submission](static/2025-12-01-4th-German-Biohackathon.pdf) +- [2nd Hackathon - (Aug 6 2025)](2025-08-06-2nd-LEA-hackathon.md) +- 3rd German BioHackathon (Dec 9 2024) - [Project submission](static/2024-12-09-BiohackathonGermany2024-TraMa-Training_Management_Platform-Empowering_Trainers_and_Trainees.pdf) diff --git a/docs/events/static/2024-12-09-BiohackathonGermany2024-TraMa-Training_Management_Platform-Empowering_Trainers_and_Trainees.pdf b/docs/events/static/2024-12-09-BiohackathonGermany2024-TraMa-Training_Management_Platform-Empowering_Trainers_and_Trainees.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dda96e47fa3c26e0c62b8c4d82298ab1f68352ad GIT binary patch literal 24710 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 Sr#2%K19L+zRaIAiH!c8m@qYmT literal 0 HcmV?d00001 diff --git a/docs/events/static/2025-12-01-4th-German-Biohackathon.pdf b/docs/events/static/2025-12-01-4th-German-Biohackathon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4c598bea45cc05c0f62799dfb5511e57cd2b820f GIT binary patch literal 27478 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 XNn%k6IJFs>Sr}V#sj9mAyKw;kZzyf- literal 0 HcmV?d00001 diff --git a/docs/in_progress.md b/docs/in_progress.md index 3a77e80..19a7126 100644 --- a/docs/in_progress.md +++ b/docs/in_progress.md @@ -10,5 +10,4 @@ ## Activities / Events -- [2nd Hackathon - (Aug 6 2025)](events/2nd-LEA-hackathon.md) -- [4th German BioHackathon Submission (accepted)](events/2025-12-01-4th-German-Biohackathon.pdf) +- ... diff --git a/mkdocs.yml b/mkdocs.yml index 34df5bd..97b3b59 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,8 +11,10 @@ nav: - Technical Docs: - API specification: tech/api_specification.md - User permissions: tech/user_permissions.md + - User registration workflow: tech/user_registration.md - Product Docs: product_docs - Work in Progress: in_progress.md + - Events: events plugins: - mkdocstrings -- GitLab From 301b53f9bf1bf0c592d85384bfda3187b7f3c485 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Fri, 8 Aug 2025 11:52:53 +0000 Subject: [PATCH 02/32] Docs Files from History, CourseRegistratioForm fix --- docs/events/2025-08-06-2nd-LEA-hackathon.md | 4 +- docs/events/2nd-LEA-hackathon.md | 63 ------------------ docs/events/assets/Flavour.png | Bin 0 -> 73172 bytes docs/product_docs/history_files.md | 22 ++++++ .../src/components/CourseRegistrationForm.vue | 4 +- 5 files changed, 27 insertions(+), 66 deletions(-) delete mode 100644 docs/events/2nd-LEA-hackathon.md create mode 100644 docs/events/assets/Flavour.png create mode 100644 docs/product_docs/history_files.md diff --git a/docs/events/2025-08-06-2nd-LEA-hackathon.md b/docs/events/2025-08-06-2nd-LEA-hackathon.md index fee8d5a..3ab4a8e 100644 --- a/docs/events/2025-08-06-2nd-LEA-hackathon.md +++ b/docs/events/2025-08-06-2nd-LEA-hackathon.md @@ -1,4 +1,4 @@ -# 2nd Internal Hackathon - Apr 6 2025 +# 2nd Internal Hackathon - Aug 6 2025 ## Morning / Afternoon @@ -28,6 +28,8 @@ - Course flavours (see course discussion document) - We would like to implement this feature but how we would represent the branching from another Course needs a clearer proposal +![Flavour](assets/Flavour.png) + ### Decisions - Adding a URL field to the Locations DB model diff --git a/docs/events/2nd-LEA-hackathon.md b/docs/events/2nd-LEA-hackathon.md deleted file mode 100644 index 7adc52c..0000000 --- a/docs/events/2nd-LEA-hackathon.md +++ /dev/null @@ -1,63 +0,0 @@ -# Action plan - -## Morning / Afternoon - -- Overview of progress in last weeks (Adeel, Franziska) -- Status of task execution backends (Harshita) -- Status of waiting list, requirements and UI considerations (Kristen) -- Status of permissions and overview of options (Renato) -- Status of Course vs CourseSession vs (course) Versions/Flavours - Sweet spot to meet needs of different organisations (Nina, Franziska) -- Review of requirement and any other topics (AOB) (Nina) -- Reflection / Lunch break -- High-level discussion aiming at decisions (Everyone) - -## Decisions & Discussions - -### Discussions - -- "Organisation" for different collaborators means different entities from the hierarchy. - - SciLifeLab would host a LEA instance and have NBIS (a platform) as an organisation - - EMBL would host a LEA instance and have "EMBL Heidelberg", "EMBL Rome", ... as organisations. -- Flexibility to modify Questions and QuestionSets can have implications in long-term data collection. - - Changing questions will inviabilize aggregating data over the years. - - :question: Should we add the possitiblity to have Questions in QuestionSets to not be removable? - - Use-case is to avoid cherry-picking questions from certain sets. Encouraging the creation of new QuestionSets fitting the course need. - - :warning: To be considered later. - - Possibility to have a drop-box-like widget to upload files as second step to a registration. Preference to have this instead of specific questions asking for single-file uploads. - - For the drop-box, the backend would only create a folder for the participant+event and not need to track the files. Any content would be assumed to be part of the registration. -- Course flavours (see course discussion document) - - We would like to implement this feature but how we would represent the branching from another Course needs a clearer proposal - -### Decisions - -- Adding a URL field to the Locations DB model -- Add frontend option to allow selecting more than one set of Questions both when defining a QuestionSet and Questions in a course -- Create QuestionSets that are mandatory/core for each organisation. Always present in all Courses -- Required questions for each organisation would **not** be enforced by LEA. The organisation would be responsible for overseeing and having a process to ensure questions are present. -- Attempt to use django-tasks for some of our tasks and consider changing if we run into issues or we can't meet our needs. Priority order is [django-tasks](https://github.com/RealOrangeOne/django-tasks) [(DEP-0014)](https://github.com/django/deps/blob/main/accepted/0014-background-workers.rst), [django-q2](https://django-q2.readthedocs.io), [django-rq](https://github.com/rq/django-rq) and [celery](https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html) -- On the type of course registration format (First-come-first-served, with evaluation, ...) we do not allow this to be changed once a Session is open -- Design and style considerations - - SCSS files using variables that we can customize for themeing. Agreeing on a few basic colors (hex codes) for these variables - - Admin and non-admin pages having slightly different themes to distinguish pages - - Design mockups to use PrimeVue widgets for a closer approximation of the final "look". - - Design feedback will be done in the PowerPoint where visuals are presented. Changes are implemented if simple enough and brought to the weekly meetings if additional discussion or review by others is desired. -- Permissions - - To remove one of the "Organisation" level roles. Pick one and adjust permissions of the remaining one as it becomes clearer. - - Allow creation of new roles with a big red warning about the caveats (updating permissions with new releases of LEA, etc...) - - Don't allow customizing pre-set roles - - Permissions (i.e. rows in table) would reflect CRUD actions on endpoints. - - CRUD actions translate to HTTP requests: Create -> POST, View -> GET, Delete -> DELETE, Update -> PUT - - We need to be cautious to include Organisation, Course and Session in the URLs so permissions are granted to the endpoint URL. E.g. `/api////register`. - - Permissions are then a combination of a CRUD verb and an API endpoint - - We need a "Participant" role - - We need a "No role" column in the permission table but this doesn't need to be role in the system. No role is "No role" - - To provide 2 permission templates (taking example from SciLifeLab and EMBL). Permissions are selected when an organisation is created. -- Hierarchy - - Rename Organisation to Unit. - - Course > CourseSession - - Implement a semi-rigid hierarchy between Course and CourseSession with two options, configurable at the Organisation level: - - Allow all fields to be editable both at the Session level (SciLifeLab use-case) - - Hide fields that directly inherit from Course (EMBL use-case) - - Ability to edit Course information (e.g. correct typos), but other discourage edits at Course level, and encourage creating a Flavour (aka Version) or an entirely new Course - - Agreed to allow templating a Session from both existing Courses and existing Sessions. (This will not require backend changes, only a UI redesign and frontend "magic") -- MVP should include the features discussed today. Possibly not some of the flexible discussions (e.g. QuestionSets) but all the key components to have the system work with role separation. diff --git a/docs/events/assets/Flavour.png b/docs/events/assets/Flavour.png new file mode 100644 index 0000000000000000000000000000000000000000..376dcba14ae040d3ca8d57d682600f0fd83bb04b GIT binary patch literal 73172 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}VqjoM-xwysz`(#+;1OBOz`%C|gc+x5 z^GP!>ux!i>i71Ki^|4CM&(%vz$xlkvtH>>200A5Oih{)C?9>v4q}24xJX`hmyZ5Ur zWTx1vhnxBu_!c;)W@LI)6{QAO`Gq7`WhYyvsN36d*;H6n#N&w*%YOuSp~VcL6sDxq}i%xl#~=$>Fbx5m+O@q>*W`v>l<2HTIw4Z z=^Gj87Nw-=7FXt#Bv$C=6)UUTad9bt&2ULAPA|30OTwM1sjNZE60>#J1zwUh$X?Pgosof&sjh*Au7SCYKGb}e z6vz&Qt*#X%o+);QW@d>7W|k(pMyY0&x+a!JM!Jc{W=Xm!$%%<+siua8rY31{gCJT^ zjd4rzOtCXHGBq+YwKOy_G`BRhFo0_SOQNa|$uCXL0C~mE2(Anv23BjM4+`C!Ot23j z9B8P(bwGj)B8L<$#79+1GCZ0JN{e#9NgyRzKQ$*cH#M&W6iJ5ANJ>ezD#9*<>x+$q?iKRIu>UPfgrA5W53Wj=y3c3o05T~N}32Kau zK9UU(Wn}rN04ZIAQXV*afzn(-N}9f*o-x!r1u1D(j-@3T`9*eaMTvP;nZ?7UU$Bq}tl( zqnnA<9}uqvxwzSJ+33Sd5j!qKIh2~0Vyj-HZf|#Omt`;mg93x6i(^Q|oHuvbD?~~U z9WU%>ZVh&H>FOvdv)wpv#flrJ=YF-3*5aDz;U3VXw4$RSgmt5$hA#`FpukICs}?@7 zH7c`n-kVPOuEdQ#p@G+&nGVKQ+DqFfg|N@ zkA6G*U;N(nZ?U^GE9;}($D5L)>bbcYCI~T`Ff0hzvB2R^?4gnuAI|@5-`D*_SWr+< zaOti7iO&1wt#YhuR0_5%cp%Yi!mvPq*@Ph^zP(@evy5$)Rml(cwg1;SxwyEvu)Y56 zQTI@y_KC-a1r3uIGcK@7ZeVN3TD~BoT&!OH!k_i3AXOoA?+5}L5EqVX^kMqGM zrnUQP*%+2^Gn+6-O!$2w@STYV$ZV@qML#%{O&BCVfhXa0aAR_1yXD83AdO$H=l^Wq zp=H7#!4J~pUUzU~{EkP)pzwUTOTf24#W!i?jcIXHim8Qd)EhWSjV728Lu%!B(qnQFf-JquUM?M-wj!-G92t zk-32pc)>=Mz^_!0MdKPNc2LVf2gcHjDsO@<-B zkLLoz0{eEZzP}QdOMb>PgM84jYT0|`pWqPVWoMYe4l=pt$G+dwYp$h(%w82I+aY6h zh?kSmp^PTZ~06{xJsG zW)w8-@SO<)3?*%hQVb=JUu;aaFZ|f+m7A?$bpQ4L8!si(O83o(K6WZ*pH<52-?QH) z=d8=rdmmI^;2-h({H%_S4v*`tJ0(mQUT}f@(KfX^Kg2DYX(}$E4^3pfcR*sc_v(u~ zm&6&}RP%M5AyoNz9KoxRbRJ1XJ|+}C=BN3us+>l1gEOyk)_=Va zlXlnl8vopPyazWtbMFDU>|b5S6M4&HnHLxq@Pi_Pc_Gs~+Zj=({~j!VvyRVF^lq#5 zvxr|$Zn8zoJeGaeKd1Beg_m-<#e37rzMR>b7E;kECZE@JdiUJb#h`edDpjt`!mt;V zvSfeEy*z(%cCm)d!Izr%OUl|vK+NfzxtO}9=~~a z{fxIyZy$``Zjn}9GfO@Q6z-P4?IzVvnY@6}fgcpfWsQsH{EI2O&*tR3rf-Vay)!bW z6S|W(o!)&&-sk(_+d^7i zc=&zus@|n-@++vimSt`={mQz6C)(@lH>j>WzbLpbwd?fipUI%aKS`Y(WaAf*v$L)p zb~pLIE$)yR+u{71b5ixTz1?*7aPB$Pm-`R79X-wEYFAoStycK;m7dyZpLM*xE-o%1 zSI+-z-}0|bmLY(RErVgg^^O03?`NLqJgYuhKQwaT35nUO&rIFsKQFiK_IB^~>uVz3 z@%L@9G(Pow@sEIKni4v_;&f8{B(^^^A6u#cO}wH z`Szc_yGtK_ovXb%_Th<=?d!wyB-LH^&&ZB_!JY9h1C$I+&J@j9UBJ+A1>}#oo$WjS zK9H|{`ZfJogxCC}^varJyM6C-)fc!#ic3r86sENHeCq@Sp6boU6N-5*91KRu4Qvda zvbJS!*w^l#J$>Qag&r@bSHH{Kbk6kGQ4O=UQ#bVco`;fi52|q{o}gN;rJx~nsa9i{mUv%>nDU>^Qzyl|7P4N9@|ULt~^d%4|1v2 zjK3V=4;dILK?#4!n)d#Te{1(_>)pEBTQd6o*4^b{r#F_)JAVIc$fKIg;o+$I&wLCTg3Kli9X|e**k$sQY}y&#dy7ujUf_bYY$hS?BzedE7s0>{IPiHLseyJY@; z%6Y_VQa=GyT80L1Sm5wF{~;qoCs>Qq)vrt!-rtB-)m?Qs_uQ+>v%QyQzx-IL@vy^o zcJ9AZ*&l8j3keDex)v8mW-v?;Wj0|raia9eif8wagcsg+o;c~?^*^dw#-ZBn`8-yS*F+Ih~8m@?x# z`zEd9`n&7s!k>qgm6VjcviCoezw|4*g25q)=K_Pm;y2FirvKIUo%(f4++)X_ZRgF- ze=T6`6uBkA^ZlG}&F5p`XTHV89BAq2=;*o4Kd~mynS-Gysez3_^6#vAkI(s!Rxf** z7k}vBmiCjo^aL*bdH8bUL$mZl&bO@3PdmEviHnO%${nsgLr3O@9#Hafv(NbZw)(2c z$(NoMQ~9=8Sj28unria;yGi8sO^(8Xf)gFBLDgIk*oMr1Z>vSWG#;K?pdy=GocZqt z&jqFU1+|L~-#(ji_YfN>54rU)N-;bErK3xhVZ3ZFHhw$u`m@S%_s_@EW`zB0o@vRM z_x8rsl)Dl<+5bL(>Kv`@he6&3H6kYTJ!F5usTxz6+;@EW&t&5W>86M4ma%MCeUq~( zhi4<$ISZ{oW`mvcQlh%SucGo%+0AFJ`x>70pPi7}s$^Syb3rF4CSS3tb{Dyl{T_t9bS1v!l9`k+Ysr>?+?>#L}J-hU0Bd9J3 zH2_&I49XG5E0{|TaSPPH{Fwdkz^X*)=7(owo?dq{IsEoARILg^Z36Qbfz{5o<$j>7 zns9Yn=)Z(Rrx&i_Y%!_N0cC)veehT@yu@I8@nzup^ZCUG+1Ext;a;&{FV^>3?41b{ zIyyQ^q(O0-!pkpX5Rc?tBq%@=MxkZd})TTS%SGFOk>i_ z#d|&~Jip;}b#~{vzE3;byTNg#2P%gw!7IaW5P`9+5Wp4lq^eLbO3$go)odds#_IjN&OXsWR&$zns$E2;N&GI=v zs)Dkg?NPY3FRB{mJ^r}be|hXf{;YooR!k{tRlXY{-duO~jOS|?7ndc=K^X?rZc>o_ z%XeY-p^1H#Vn_Qd?>)Tsr!jD*?l<)* zk2XK4yx_Qw#gk7i`necX<9Zpv1Jr#v^Os`@5w%=zmEs*fv%7h2vlV%{;1`$QgS-}y z8zv*%FqiMb`jzi&Ev}q$Z7IF>Tu#1RSw%@nX)YolUlca{J2w3R*CC$iRcp%U`F<>w z$t%wKw*ut6Q%KHx%y8F#YgExowz=DsF5cz1&gu*e#Y>17EHQV`|HLX%ojUzcHGkIs z11oBHH*39jv$%2SY6z%B^9|v!ySx`(U7m0M{f9|?NXR=gm0k0naMcHVW_JVCv0k9u zt`72V-#)e%s;jikN=ze7>q9~gw%wkh?zsP)=LHuR7nO1pW!KO0`PG}nar!sKi9hfH zE67JE=wo|P+9NdS~TDL3NBLDCNxoIm^mQYJvMIvs0RS>!0;^X{mno z7k9bsEO=I40;KK`NC_lb_wrnbT-cE`_sxXdd$MT@O#GNbuZRCUa*|&H@9>_}{g$Y_>h5L#FCECQGg(+R*J|mS^M-fht9?H^ zrz~;1HK&WWYF@kX+0Kp*k6nlm`N{rbr`-OeiE|!&S@KocFMg|+t;zLu!Ou^HU7uh2 zZl`J1m4H33E-yW8V(-nk{``sh8`K0& zC4KVND_mn&SatkHOtWs^^fTM!)Uw?!8n5a;c}e~Jw_QgKPd%IU;ET?)xamu`DT~Z} z`jL05Rot1aWv4c3{QKNxKyY0$WT)ARYi_k6vUJagvuJH_X1`|Ym1`#CSa-Yj{$ z{l}}TkDvXu=y&#d*V((0S|4paU2h{$o%{b{@}qdu^vlm~tUh*ro^sr#)30Z5D|)J( zd{0b1@9g^hNB4gWn_c_jNN4Wu(%nz*Rll!)baEXtTivD#Pfvc$SwCcd@yY+oXySF)&Ae_zj{=)!Yf zen>19?X))$t6+WE_(|eS3Y^A8Zy~ zKX*?~<(sW%&s*#N3(c=HQT(>5{r2|x`)l1Nr==f@D>~OI?sh*X-{8xk35x3TYBv2+ z?DRK&cvoceoOS=VWruH0^ZmMe%dWTEp4!WKT%Mk=@s*a0a?ZIebr((Zi#2Sny?Zz3 zRDx9ee4E=U%hUb0+IJS^tNagMzPv&?k=ZU6i2d$V|KU8QFJ|Lv3gW^LEi_KojI@2Z(vi#BWmb7J+9Wti5QGbp_^-Zq%-mZN8UXk~o{Gy`0 zRpGJqhb_h9R#;56sM^^RG1bUn^;bDFpFL-fxZXdq|HGu!>#CL=|GfQP`F+pQ*KYAe z4^yYFJ#Tol?!0W?dC}`};{Wx`@0Et9&ENOk{$on>vEF3;J-?1!%lzfU6Y6XEx+(L| z!Dh9$-B0`rE3b<0S(jkzbFFfokpFG-l+%9?+KJk|HA$2$*0~&W@73m)`JAp*Ps46? ztj}0-cIV}e=6`(!bxY^y&fAa}_S`Mcz(J)D;juH!Uw$P-eBAyq&@4R4{!v5uv19Ak zEsdA4OjKU()A?U^*ZyBwZ$eMk{rGt3v|jh#Ri2xTzaQ&eHPyml`@Y}r7RvkoDS2u1 zF+uF@P3?QjerK;g8u#0$|9SHJ-FKg!d-3%Ut8Ubh_2(BgCLcd~Z(rKlDETwLwZ5&= zY5#mG?E3LIZM!O~eFDa|c1NeztvWsXntrY5c2kjebGI$^|I&7JBVxvA{tv(|gBzACFK$s!s> zCu|(f7ifo{D4Boj=HIK~$(LVg@=8s4T)yMy{r`P``Tke^5ImYK{{FUnz1)`;R_^5W ztJX07y7v3s?bTM17MuO=-;n01_&wLizWc}J`SayXJU-s~zJHFmecAHI+!IB+H1>VT zkCM&&dnr8CyY}GoOMAX-)!p^yUE1RN7v9*mTiY z-tH6nm->C4gTX)Zm~V&syUc&2z2A2ud;N}YyG&|ztHkUbEsT%Pxu#z`)%57o+Ff2^ zt6w-q=Uq5`;vK(x+vNjyW;mv8CDkFH-;{k`A&<=y2v z|L^<6Y)(7dvRq=n)w{blZiAXIH%@D(Htc&dE8D9oXY;jui`RFGh5JtQ&N7oVf8Bi} z+Ansa(oe=;hrIQDFJDiZ_R8pjajD%Bzk({0$X_m}drDuud-5gz`_3g*GuBx1me^)| zJ$h~RCjBozw(oZ zcHybAo6bsyhO=GI?B2u3+rQ+6=;qq`S?gItOm)ArZ?&Ct{>2A(<=uxT&U?A1He=@2 z^JeR>-F8$qDnJw`TbRGN`#rViS>IIuXW!kYb}RGl-uUy&!_7@I|IWLg^B(1{+q3TK znf&Xn@tb~4kAJLw*R}fFs{5}`>cuAI7o1<*e>5)c7sKCU?DAnX4Sientkd6|iG1{N z{;ds(Z)(@)l-7RPy1MS4>c7nb`D?BgD{TH@U-UkITlQBEts7GH8|E(aj*nhEXV-2! zfm0WLd33qmoHI+li))*q$h!qcT~$Onk8jx7(tXkU#=n9mrQOb-o3EWrxtX=Y`=G!} z6GRn#i?M3MtM@-cKNrq_`LX8DpI;{*sdC=W`MB@za+3^!>dlq6H!af7ulV(Hqxe3X ztSyiBerr8cZ~y<#4e2j(W;)+ixf>ZJ>7RddIs9Yacbi`ktn4pr8y}pSUl+9J!|AoR zv#zcAdHm!#)9hntvRe}8v;B!%7=Qgdc&YtXtgHJ)h1JfV z7dyJ#2PLO9pth~mCB6%`UHZPYpL0S#7l!})68y*Wf@${Es=5mc_8!%b|M2rx{`q|m znr_^VxwR?vO{jm#JDKXAl4obH|M_+MkMcM6RX%@q?R~QQee(KO|6Z-$IDLCgZSbG! zPIF=Y#Y)Uy{uMkqxcrt`X4$h(^86ZcqV+;w)Hb{RjqLayG>2vH>DRk|9lMr3ulU>% z*ZWJpW`ErF%C054w1V$_!0K#;Lyf7#tyc=)RMwy-^APp+)o_@(m zo9A`ympdVOcJ}lyk#lZsSXd$P=wL^?$(P5SAJq%L%iF5e#&^8Tdn$XW#4zOO_GL|r zuO3_yKO;uASf~2XE%|9hTLMeNFNN?W$K8G-RCb9==8R-s>^r$#PqOdvpZe7- zb)Vy*xD#8m>U>VU>HQR6qrCpbg~(fC|GYaE)$;^ae4KnJUgX^Or?qVRW<;8PbwcTyo&xg~ObIm`XDSzhX%l*f5T%%&oNt;cpmC37lfB65J`?JnO zubQp--BEcJA1M7odcH+`7qmm`Z_j`JHT+}pj}NPVs$X9ne(F!uVz*vC{cmCWAD2!4 z-RU>itMa&7x{2`qoxfz`B|S}6d%)HLd@eh*egB6`-qZhnNPbg4?J>`b^_#XGk2iLC zYwvHrcm0pG2g-KXF3rEdD`nbsx7N@%H|Vd-quTb}UGv#QGb=6KS8w`#{f}$;nK_f6 z99pWOe0$CA-GZz9>;GLpbksL5r`GB3uX^jfnVWaN+gJb9>S_9{BQJL?I$ix;)c@J; zXXpC&d9Pys?7V#5ya|iuKJ0jOuetx-&vifl^3S`m=H|z*ewlY({fbui53lL-Tz9hk z+$`xvhr*WRA@?w;+x zvTmopm;JK8*Z6;Zy5#c2N0|m^qDsS)R#xQSoYQr@{;&0WvmK94%~GEi@qN$dbM>!Y z9^e1p^6E*=sL9gN|G&1}^8Y(8dSChcW!tqK>mL+o+qGU^w`bKoR`Gj3?zLa%kq-Pn zN$h*g<~23#i_Z4%zh{~KZu0(=>F@8(KWp~-wbupXkmdIO7blBOD&H5dBb+3l@Qof_}4}((Dv5&{icRyY?Titixp7Y+k>vnwF_?P|BJ%!e-@Als= z`@<{C-|vPld*9{;}j)y|{Z&;jFiNy~9j$Dt#Y%uACl!vFL-q$4T}2 zQMaD7UAuTWK{FC?tgdh|9AHP#Wj0_U(G9CSMjZD z`_9BWzn5>@u_f&8E>Ld!|Elz2a_Qk6-xsy-eP|zj&Fbfq$>;5UZ;IRd|Hq`cmDhge z&d)5nepmg<`ul(OvCI8`r+!pfGPyXju108C+}oqUt~Hy=>%W$6y!-QfdidMRe%7z& z{g#W_ANjW?*hcZcg^s_iZr#Td>;67^(D7X~zHh(%kAN@BgHz>9YXAE5ov1)5l_t-b z_hWx@ddh6mdXFWZf6spVk?s^enJ+I)EO(-RrP%-G*XH8uX8vW@h^sri<2xv{qSWW# zd2;O9_IfjK+qWyX?=P0CTywATxp-dO{^xO1Zhl;{c~-Z1-kn#!*srY-*YD|Db-h=j zHfeL(*Z+l{ufN*|onIFDYbV$7YcAX6_k8&&e&qR&>9b6wx9xe>wNKx^)ZqQe_)SNj zS0BqQx7n2apY{9}{XHAb9LqiXzWTwI)$7FccRpGgeq`dF@|UL;P5kn=divTfxsx{- zYR{XWKB6xbfBn_3jgO^c3R8O@_k%6(UAOn$vVOI>f9L=2inpJ$)%&AINlE04zZ3s_ zTcuoi`^(JwKd0O3W0d-CFRtD%t}k+ayH&50{T9vDS6`GLS-q~lI_C62=YLVVw^i_U zI}6sjUA23=h5NSs@B6vd`Y$i7J=++c%=|^zUS8zA_Poh!-S+DC?XO6D&9CON{mOn* z%fG+M?}^5*KUBZ&<2&{)_e(cZF8&cv&6yCo^OpUl-{O_-N-jPB=;@=@ z>pYd-d!Ag=oZtC;l7+?dt(8IGIDO3iV)MJ5@7I^U>adrODyjdcZ<^JSJo}8}>j}v> zm#;4V(8(hPpD<8<&an1}_;=ec#@0bkzomVA>F#ddRbhS6_p?;|^;I8)^wj6w_;T%b zbp4OQhn~B=!~ZRH>-DbQ`{Tm1HEp`9d^^FD?tHSga9oqb;GLHt==R!b408^O+ZJbzS>&bLhVX8Bgc0)yVoe zUspG4O>E`g-VAg9Sv7&}&;9Iv%X~Z;_xIO>-_zEM?awHC9y|T}J89Vk?|;;MvuEEI zw>WIA@&8i))q(l;|Nfruum66z^X#mv*8iCb6BdWR?clwjdtXMaPSAG!pLP1r-P13> zSW$Ji!?4x)?G?$^{0$GJj~=d2-2TCTx$3_MN%0?~;|nJ0EBxBEqIKT^ruYA6z5iMt zdd0>+F}~_gardm?8U7}_S47sEZC-KScAwj9{+!7+F7^japh07nHQ_~%&+{u|CLFW&b;dFe)GOvdB1Zbqt&n09a#smALY*b zW9n{mJ30E5CBMB^o#MPpuK6EU{hHYE+_PzUyvv{ae`Fv2dHe9sngW$u+X^0D*8jzG z=ht&%gI`~6e|dgc{~J$C{1+abnqRE{GkgPAH$M!0HYp4-0;zEM|W@g zzkBiio|n4YJ0H(~;uyUMGeQ3K`*_kbUOaArS58ls(eFEttB>YP-=8kj8`fX{|8uFb z+yB1@+x=F=|JOIo`cr-J(u;SMiNDkJ-{1Y-bNijZ{<=@*lap=*=hts!-1U>0UoQUr z6!rSEg8TO^im(6puK&#qY1z=l;?wpniSCb5pSz?^rCFy>HFLLn($i@l((=k{E&q4U zuhXAnQGe2I>(QmlV@tp8JssA0ewXX_=kMp3n%_#Ut{2F^@au2?to#2LSTv;HsayVI z{{Ndh-foK9`+8n^+!rCP|I!QC`8DrMf37c-U;O&X%hvSS`@SdsNmb38{cY#3RooZN z)UDj34++V67CQXfT<87ey130`MZ4q2_y1m5cDU_P!O^}a^=3Ju>qY19u4opPGO>7g zC3wEYhuIZI@qdp^IJS0O|Gew+)?pVf*De13H2d>mebcO~LhrAd3(P1-FKt!koK{ff2|e!<_7Hl z=PEnTyMVLLF!}th@Ba=h|Nevjwp4b582`R&p6apIHB)}@f7@f`?>1i|e1F`J^MdOn ztz!0k03}X;6>v_-S-p$pxIOzmy??Vs^J@+oznyH8!K`Dodx`bLiHM=4Co&5}%TGz= zJ+||{y5*$a`lQ^->kFG1trE-6M@Lv#mA@%^zGi3menS;2^}o9_zVCVPvg}c-xc&8? zi_(oji6!Y)a87;yn_KgL2+8zHxu2iqlhzL zZqMz0S6hCnRlIZh6^nmAy63+Rsi?Up5%l`WWbXB1v5SwDDj&1iqN#25n)^}2)}If> zkNR@>XIyajbAG?6rR_hpmQ=nAuV!C48F`2?s%BZ~d0X=;pI7tB=T&Uls=G@u?n_P0 z!k25e+iv@RK-T#AoRYB5FTBrHekuH@)62L1(8to(cR!ysI_k@D^s{l*)|uYb`y~E4 z?2=s|Typ=@Ro}b}*}q?o7C+l0{<}lm?&t28i_Y}b+b%5o;&E?#Qtz*y%PO3<>DCD! zTDJX;RPEKs?{}8(aB$aTUn~CKsl#J8O2bw0==wj_O^myCncZGI=bP6qr44^R0T3FKoa4=fmk>_Ool<_6y&&^ zb2DELN_7Wjef3whbvXa+hpD|35m$9txNAKz$U*)W|u;su^2vs9GHPf8~$jL^a)@xl65&&HLe&6}0r>qiKJ* z%xRL$<44DV6fO`s#JI_l#-j4(flWIj`wU-)a{yY58NusNMbs zKkpZ38?3=%l%WydBej;RTXh$9BxOkk{%IH9wf>WS`GhyouO7t|#a_9eV}6NY?lj#{ z{^x?>D@tmbJpa$jZdN?|e~sFzc_12!D#Jso(!d4)LdactNLcnbx?q%t~}Rp?I^?A z?wenjZSPp=|MYi1oUAu*@+bZax8#&!&+Pb>tA4#f?UDv^*i`LVe1qYx_4#E#ot1oV zNpNW|n|85k2V2Rf@Um+KGiJXry5Y;A{ZoqLkdaZ#cX2+9u#K@$YVGyho$YY`(+|`CU$j47UipWA z?w9G$f|rMjGnaU^|NZ#BF3YmwMCaLAb8Eg?c3rpoIr}B3{aJSPDyS%}H_Hj#^X-*m z{x$3WO;M$<=hc1I-Cl2|y;NB@R|j?JIiUkh<-F_=P{0*MB)xuHssLKDy%B{&$x9^7j6kzGX++jgD?l zJ-)n~(Q4`InKAcvzmLzlvf}QBg}(~!T4t}SICVAiO2pqI!t6hn7@rq-l1Wq>L;cCyI%GA_-b`#Hv_!qQu1 zF^@Ox+7?hc?cHxbS)L2I!8_j^2xj+|{vC9Saq|BKdyCdeH$4nLQ}*+9)+Uwu8>h=v zT>IbL5&wJC)cKxQ=7sk`UmkDt^gG|O!>+7K&B}K2zc<3mzh7Q-WPZ-YWjpo+$KK0q z{kd#s?d9%YXWq{Km_EO{#<+Xd*Yx;`n$4hwV|`v=eAS=Niz@%di<;hl9shvWMfm^E zn%FCg7A;#+clG@wzUeELFh-RGwm+BJzAJ9ew`ZP{PyY~L@BUge@7sBESN)l9Ps*-J@h%o|tz?$mUaGSo$a7OTnqV zwNGy|dcS0DkN&KmH%Zt3ug}|0vM0B>DwqECjxyQx?A8SoQV z6`yQBM)g+xp0w`n>$&xghwHc1y!w7e#Xc!MQeX9=`c&RsUzCF{t^Rj${GuYD7@O{vqHH2szE z`m_A|W9-XnzFar={o%6bpzVDf?@xz2|KBjRx8wXB@O62>%cWKh_qW%S_C{@8C--I9 zkJ44lUl_V$KR~`23yVD^W|{prPb?dvzJ)(h^Lm-Ji7j7iruuhIwhgE+E>qi^Lo{|Rr=i= z)9ojF*Zwc73jWy5zW;8;`%R&b>i_+It=j+Qj(VI@-&Bhv^L^hJR-6=OugfmGe)oE> zO!0H^_%89dd%M?HG(Y=puTl5+z^t#Hbw{o;de)!XaYx?krRKy}47INN{~Z?%|3690 zvu@4Fy!p?Lx3925ERZ>2>9C$xQ_H!=e_i2^XfHm+btf=XZy8F_UVI1{QGK>@A|PXoVVib`mNdB)34jT-Ljf( z&#Win>$b1Ce!t^e(mdUn=NC=M+&3lsZ~EhHH-!CdERS;k-&#;;B)=|pTV=Jznx6+1 zSDiyiSxA^qGfu@9h2&P}Cen0(jd350ehxyy@Wqtd; z|M>r~@Ae;;Je!}Ede!>+s>M^i0zZlC>!rT&&Sdxbba*ZQtNblhwmVJt*SLS$f4fY} z&#~4byWB`VZ!+KW{Wn)>zfr$>aF5FUPb=S~+&9|ywCKO|^!0x||8l5YG}*6jb$0r` zTiefS?&3}En*QLz#i@5Xd!&$i{<=Ybe1GTuz4LPQ6szy`OCP>mEe<}M_yv1}?z39X*2X$WO z^8G6mJ`p0l__qC&IGumRbGht{|JzhmKAkc9H8YzaQc-5+=9 zab3;%XAxy8yZ&klh9WJ7+QMAIAH1=1$^M%h+oN_ZaP4lHTxiMmLfxud@8x!*GYo6( z1mkYqo-*~yv49;llVdI#O|7`7{r!ykDUCnozpHko71peb;krxJc%byT88rXI?l^!Tg13eUaMi=O45Gtq989 zb2#?n#+93Xxtw2o^;kly*nv}TV{7Mm$h}V89Q!$0!q@%!MY)i5^%o}e85QgN+Vxv? zVjv>-FOgkvKm2T2bV>ET#5LSeTPzlqy$#!b_uILi^WWK=pU0F0va-Jr`~T

BVmO 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 wm3d2!iL-^tmRmUG*S?%e2o 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 literal 0 HcmV?d00001 diff --git a/docs/product_docs/history_files.md b/docs/product_docs/history_files.md new file mode 100644 index 0000000..9167a19 --- /dev/null +++ b/docs/product_docs/history_files.md @@ -0,0 +1,22 @@ +# Files from History + +## Links + +### Goodle Docs (in Use) + +You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Currently (August 2025), the [Mockups](https://docs.google.com/presentation/d/1Eo8a7EYEBldZt3N1C25U4ZWD9tZvSRe6/edit?slide=id.p2#slide=id.p2) by Kristen are in use. + +Additionally, the Docs includes: + +- the [History Mockups](https://drive.google.com/drive/folders/1-i1jrp6Hq-1PqwNLmR0NIbgCU3VNBzAM) +- the [BioHack Slides 2024](https://docs.google.com/presentation/d/1mqFrpAzkHG1ZrWDoBcUgmbwkxwpKALAWF7p3kIwyhME/edit?slide=id.g31e313494ed_0_536#slide=id.g31e313494ed_0_536) +- [Design Slides Oct 2024](https://docs.google.com/presentation/d/1dHXmiBt3N2GuJq-y82ZebWfW-koozP28ihJh4MJPu1k/edit?slide=id.g2a675a1bca2_0_14#slide=id.g2a675a1bca2_0_14) +- An interesting collection of [name suggestions](https://docs.google.com/spreadsheets/d/15h_V8Wym0OLns-QZUu9bbwaoBNHYn66T04IxANfOQSc/edit?gid=0#gid=0) + +### Miro Board + +You can find the link to the Miro Board [here](https://miro.com/app/board/uXjVNEir6Ak=/?moveToWidget=3458764572809405712&cot=14). It contains the design, wishes and needs, Database Structure and Issues categories from an earlier stage. + +### Cryptpad + +A Documentation file via Cryptpad with additional links to proposals and files that have already been included to the docs can be found [here](https://cryptpad.fr/doc/#/2/doc/edit/leACtzApMYx1ZzPkpGCtx8yr/embed/) diff --git a/frontend/src/components/CourseRegistrationForm.vue b/frontend/src/components/CourseRegistrationForm.vue index 6ba023a..da631a1 100644 --- a/frontend/src/components/CourseRegistrationForm.vue +++ b/frontend/src/components/CourseRegistrationForm.vue @@ -34,8 +34,8 @@ const sessionDetails = ref(null) async function loadQuestions() { try { const res = await getSessionQuestions(props.sessionId) - questions.value = res.data - for (const q of res.data) { + questions.value = res + for (const q of res) { answers.value[q.id] = q.type === 'multi' ? [] : '' } } catch { -- GitLab From c524ad1b3c62c9d2dc2669db045a8991acd0716e Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Mon, 11 Aug 2025 07:40:27 +0000 Subject: [PATCH 03/32] WIP Glossary added, clarified terminology --- docs/in_progress.md | 1 + docs/tech/glossary.md | 28 +++++++++++++++++++++++++++ frontend/src/views/CourseListView.vue | 14 ++++++++------ 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 docs/tech/glossary.md diff --git a/docs/in_progress.md b/docs/in_progress.md index 19a7126..51dda88 100644 --- a/docs/in_progress.md +++ b/docs/in_progress.md @@ -7,6 +7,7 @@ ## Technical documents (TD) - [User centric flow for course registration](tech/user_registration.md) +- [Glossary](tech/glossary.md) ## Activities / Events diff --git a/docs/tech/glossary.md b/docs/tech/glossary.md new file mode 100644 index 0000000..24ec95a --- /dev/null +++ b/docs/tech/glossary.md @@ -0,0 +1,28 @@ +# Glossary + +| Term | Definition | +|--------------------------|--------------------------------| +| Course | Represents a topic and its learning outcomes. A Course is not an event itself but can have multiple Sessions. | +| Course Coordinator | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | +| Course Manager | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | +| Flavour :question: | Choose title (Flavour or Version)! A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. | +| Helper | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | +| Instructor | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | +| Interests List :question: | A user can express interest in a Course. Once a Session (or its Flavour Course) is created, the user is notified by email.| +| Mandatory Course Questions | Questions linked to a Course that appear automatically when a user registers for a Session. They don’t need to be set per Session.| +| Organisation | Refers to entities like EMBL or SciLifeLab. Note: Not officially part of the glossary since LEA doesn't support multiple organisations at once. Included here for clarity. :warning: We might have used this term in a different way in recent discussion! This term is defined here for a clear definition in the future. | +| Organisation Coordinator :question: Admin | predefined Role :warning: needs to be clarified. Permissions: see [User Permissions](user_permissions.md) | +| Question Set Editor | Allows a :question: Course Coordinator or Admin to create, edit, and delete Question Sets and assign mandatory sets to Courses. | +| QuestionSet | A reusable set of template Questions, currently limited to registration Questions but may include more types in future. | +| Session | _Always_ corresponding to a Course. The actual conducted event instance of a Course at a specific time. | +| SessionQuestion | A Question linked to a specific Session. | +| System Admin | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | +| Unit | Internal division used instead of “Organisation” to avoid confusion. Definition may vary between contexts. | +| Version :question: | Choose title (Flavour or Version)! A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. | + +**TODO:** + +- Add missing terms +- Discuss open Questions → :question: +- Order alphabetically :white_check_mark: +- Possible implementation of [tooltips](https://squidfunk.github.io/mkdocs-material/reference/tooltips/) for displaying the definition while pointing at a term from the glossary in the Documentation diff --git a/frontend/src/views/CourseListView.vue b/frontend/src/views/CourseListView.vue index f838fbc..3585e2d 100644 --- a/frontend/src/views/CourseListView.vue +++ b/frontend/src/views/CourseListView.vue @@ -154,12 +154,14 @@ function getImage(i) { -- GitLab From 78489c755fe34f3c887ebb107c35f761397cf5d8 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Mon, 11 Aug 2025 09:32:05 +0000 Subject: [PATCH 04/32] Update History files: Link to current Slides by Kristen --- docs/product_docs/history_files.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/product_docs/history_files.md b/docs/product_docs/history_files.md index 9167a19..326f318 100644 --- a/docs/product_docs/history_files.md +++ b/docs/product_docs/history_files.md @@ -4,7 +4,7 @@ ### Goodle Docs (in Use) -You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Currently (August 2025), the [Mockups](https://docs.google.com/presentation/d/1Eo8a7EYEBldZt3N1C25U4ZWD9tZvSRe6/edit?slide=id.p2#slide=id.p2) by Kristen are in use. +You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Since August 2025, the [PrimeVue Mockups](https://docs.google.com/presentation/d/1WsTwzoN9RQ4xwbgO-Ge6kpLYXH1v4kkE/edit?slide=id.p1#slide=id.p1) by Kristen are in use and open for comments or discussions during the weekly meetings. Additionally, the Docs includes: @@ -15,7 +15,7 @@ Additionally, the Docs includes: ### Miro Board -You can find the link to the Miro Board [here](https://miro.com/app/board/uXjVNEir6Ak=/?moveToWidget=3458764572809405712&cot=14). It contains the design, wishes and needs, Database Structure and Issues categories from an earlier stage. +You can find the link to the Miro Board [here](https://miro.com/app/board/uXjVNEir6Ak=/?moveToWidget=3458764572809405712&cot=14). It contains the design suggestions, wishes and needs, database structure and Issues categories from an earlier stage. ### Cryptpad -- GitLab From 659f90c78a25630b18bf940576fc13634b56683a Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Mon, 11 Aug 2025 09:48:17 +0000 Subject: [PATCH 05/32] Update 2nd LEA-Hackathon for coherent terminology --- docs/events/2025-08-06-2nd-LEA-hackathon.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/events/2025-08-06-2nd-LEA-hackathon.md b/docs/events/2025-08-06-2nd-LEA-hackathon.md index 3ab4a8e..e63d908 100644 --- a/docs/events/2025-08-06-2nd-LEA-hackathon.md +++ b/docs/events/2025-08-06-2nd-LEA-hackathon.md @@ -15,9 +15,9 @@ ### Discussions -- "Organisation" for different collaborators means different entities from the hierarchy. - - SciLifeLab would host a LEA instance and have NBIS (a platform) as an organisation - - EMBL would host a LEA instance and have "EMBL Heidelberg", "EMBL Rome", ... as organisations. +- "Units" (previous term "Organisation") for different collaborators means different entities from the hierarchy. + - SciLifeLab would host a LEA instance and have NBIS (a platform) as a unit + - EMBL would host a LEA instance and have "EMBL Heidelberg", "EMBL Rome", ... as units. - Flexibility to modify Questions and QuestionSets can have implications in long-term data collection. - Changing questions will inviabilize aggregating data over the years. - :question: Should we add the possitiblity to have Questions in QuestionSets to not be removable? @@ -55,7 +55,7 @@ - We need a "No role" column in the permission table but this doesn't need to be role in the system. No role is "No role" - To provide 2 permission templates (taking example from SciLifeLab and EMBL). Permissions are selected when an organisation is created. - Hierarchy - - Rename Organisation to Unit. + - Rename Organisation (instances of EMBL/SciLifeLab) to Unit. (Updated definition during development: Organisation = EMBL/SciLifeLab) - Course > CourseSession - Implement a semi-rigid hierarchy between Course and CourseSession with two options, configurable at the Organisation level: - Allow all fields to be editable both at the Session level (SciLifeLab use-case) -- GitLab From 43444adb021a6a8b774abe4d6b359772d498a896 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Mon, 11 Aug 2025 15:47:20 +0000 Subject: [PATCH 06/32] restructured glossay: Users Guide - Histology&Dev --- docs/tech/glossary.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/tech/glossary.md b/docs/tech/glossary.md index 24ec95a..a4fa14b 100644 --- a/docs/tech/glossary.md +++ b/docs/tech/glossary.md @@ -1,24 +1,33 @@ # Glossary +## Glossary - Users Guide + | Term | Definition | |--------------------------|--------------------------------| | Course | Represents a topic and its learning outcomes. A Course is not an event itself but can have multiple Sessions. | | Course Coordinator | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | | Course Manager | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | -| Flavour :question: | Choose title (Flavour or Version)! A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. | +| Flavour | A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. Might have been referred to as term _"Version"_ | | Helper | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | | Instructor | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | | Interests List :question: | A user can express interest in a Course. Once a Session (or its Flavour Course) is created, the user is notified by email.| | Mandatory Course Questions | Questions linked to a Course that appear automatically when a user registers for a Session. They don’t need to be set per Session.| -| Organisation | Refers to entities like EMBL or SciLifeLab. Note: Not officially part of the glossary since LEA doesn't support multiple organisations at once. Included here for clarity. :warning: We might have used this term in a different way in recent discussion! This term is defined here for a clear definition in the future. | | Organisation Coordinator :question: Admin | predefined Role :warning: needs to be clarified. Permissions: see [User Permissions](user_permissions.md) | | Question Set Editor | Allows a :question: Course Coordinator or Admin to create, edit, and delete Question Sets and assign mandatory sets to Courses. | | QuestionSet | A reusable set of template Questions, currently limited to registration Questions but may include more types in future. | | Session | _Always_ corresponding to a Course. The actual conducted event instance of a Course at a specific time. | | SessionQuestion | A Question linked to a specific Session. | | System Admin | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | -| Unit | Internal division used instead of “Organisation” to avoid confusion. Definition may vary between contexts. | -| Version :question: | Choose title (Flavour or Version)! A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. | +| Unit | Internal division used instead of “Organisation” to avoid confusion. Definition may vary between contexts. Might have been referred to as term _"Organisation"_ | + +## Glossary - History and Development + +| Term | Definition | +|--------------------------|--------------------------------| +| Organisation | Refers to entities like EMBL or SciLifeLab. Note: Not officially part of the glossary since LEA doesn't support multiple organisations at once. Included here for clarity. :warning: We might have used this term in a different way in recent discussion! This term is defined here for a clear definition in the future. | +| Trainer | predefined Role in a Session, currently equal to term _Instructor_. Permissions: see [User Permissions](user_permissions.md) | +| TraMA | Training Management Platform, prior name of LEA - Learning Events Assistant | +| Version | Alternative term for Flavour: A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. | **TODO:** -- GitLab From d246fff3e69d21a92a3d9eff4bf92b9eb5adeca6 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Wed, 13 Aug 2025 08:34:27 +0000 Subject: [PATCH 07/32] rename Organisation to Unit, Instance to Session --- backend/backend_trama/admin.py | 4 +- .../backend_trama/fixtures/sampledata.yaml | 14 +++--- .../0002_rename_organisation_unit_and_more.py | 41 ++++++++++++++++ backend/backend_trama/models/db.py | 12 ++--- backend/tests/test_api_tasks.py | 8 ++-- backend/tests/test_api_validate_questions.py | 2 +- backend/trama/api/__init__.py | 4 +- backend/trama/api/courses.py | 25 +++++----- backend/trama/api/organisations.py | 28 ----------- backend/trama/api/schemas.py | 8 ++-- backend/trama/api/units.py | 28 +++++++++++ docs/events/2025-08-06-2nd-LEA-hackathon.md | 2 +- docs/events/{assets => static}/Flavour.png | Bin docs/{tech => }/glossary.md | 12 ++--- docs/in_progress.md | 5 +- docs/product_docs/history_files.md | 2 +- docs/tech/user_permissions.md | 44 +++++++++--------- docs/tech/user_registration.md | 2 +- frontend/src/api/coursesApi.ts | 26 +++++------ frontend/src/api/organisationsApi.ts | 6 --- frontend/src/api/unitsApi.ts | 6 +++ .../src/components/RegisterCourseForm.vue | 24 +++++----- ...Form.vue => RegisterCourseSessionForm.vue} | 22 ++++----- frontend/src/router/index.ts | 8 ++-- frontend/src/views/CourseListView.vue | 2 +- frontend/src/views/CourseView.vue | 8 +--- frontend/src/views/HomeView.vue | 2 +- .../src/views/RegisterCourseInstanceView.vue | 7 --- .../src/views/RegisterCourseSessionView.vue | 7 +++ .../views/admin/ViewCourseSessionsView.vue | 30 ++++++------ 30 files changed, 210 insertions(+), 179 deletions(-) create mode 100644 backend/backend_trama/migrations/0002_rename_organisation_unit_and_more.py delete mode 100644 backend/trama/api/organisations.py create mode 100644 backend/trama/api/units.py rename docs/events/{assets => static}/Flavour.png (100%) rename docs/{tech => }/glossary.md (75%) delete mode 100644 frontend/src/api/organisationsApi.ts create mode 100644 frontend/src/api/unitsApi.ts rename frontend/src/components/{RegisterCourseInstanceForm.vue => RegisterCourseSessionForm.vue} (96%) delete mode 100644 frontend/src/views/RegisterCourseInstanceView.vue create mode 100644 frontend/src/views/RegisterCourseSessionView.vue diff --git a/backend/backend_trama/admin.py b/backend/backend_trama/admin.py index 5467b49..2eff0d5 100644 --- a/backend/backend_trama/admin.py +++ b/backend/backend_trama/admin.py @@ -4,7 +4,6 @@ from backend_trama.models.db import ( CourseSession, FeedbackTemplate, Location, - Organisation, Payment, Registration, Role, @@ -12,6 +11,7 @@ from backend_trama.models.db import ( Task, TaskHistory, Theme, + Unit, ) from django.contrib import admin @@ -20,7 +20,7 @@ admin.site.register(CourseSession) admin.site.register(Course) admin.site.register(FeedbackTemplate) admin.site.register(Location) -admin.site.register(Organisation) +admin.site.register(Unit) admin.site.register(Payment) admin.site.register(SessionQuestion) admin.site.register(Registration) diff --git a/backend/backend_trama/fixtures/sampledata.yaml b/backend/backend_trama/fixtures/sampledata.yaml index 589a663..d9f9721 100644 --- a/backend/backend_trama/fixtures/sampledata.yaml +++ b/backend/backend_trama/fixtures/sampledata.yaml @@ -14,8 +14,8 @@ name: "EMBL Theme" primary_color: "#007bff" -#Organisation fixture -- model: backend_trama.Organisation +#Unit fixture +- model: backend_trama.Unit pk: 1 fields: name: NBIS @@ -24,7 +24,7 @@ currency: SEK theme: 1 -- model: backend_trama.Organisation +- model: backend_trama.Unit pk: 2 fields: name: EMBL @@ -100,20 +100,20 @@ fields: title: Python for Data Science description: This is a beginner level course that introduces the student to the basics of Python programming and its application in data science. - organisation: 1 + unit: 1 - model: backend_trama.Course pk: 2 fields: title: Hands-on Experience with Ruby on Rails description: This is a beginner level course that introduces the student to the basics of Ruby on Rails programming and its application in web development. - organisation: 2 + unit: 2 #courseInstance fixture - model: backend_trama.CourseSession pk: 1 fields: - organisation: 1 + unit: 1 course: 1 trainers: [1, 2] title: Python for Data Science-2025 @@ -134,7 +134,7 @@ - model: backend_trama.CourseSession pk: 2 fields: - organisation: 2 + unit: 2 course: 2 trainers: [1, 2] title: Hands-On Experience with Ruby on Rails-2025 diff --git a/backend/backend_trama/migrations/0002_rename_organisation_unit_and_more.py b/backend/backend_trama/migrations/0002_rename_organisation_unit_and_more.py new file mode 100644 index 0000000..b6c36ae --- /dev/null +++ b/backend/backend_trama/migrations/0002_rename_organisation_unit_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.4 on 2025-08-13 08:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend_trama", "0001_initial"), + ] + + operations = [ + migrations.RenameModel( + old_name="Organisation", + new_name="Unit", + ), + migrations.AlterModelTableComment( + name="location", + table_comment="Pre-defined locations configured per-unit", + ), + migrations.RenameField( + model_name="certificatetemplate", + old_name="organisation", + new_name="unit", + ), + migrations.RenameField( + model_name="course", + old_name="organisation", + new_name="unit", + ), + migrations.RenameField( + model_name="coursesession", + old_name="organisation", + new_name="unit", + ), + migrations.RenameField( + model_name="feedbacktemplate", + old_name="organisation", + new_name="unit", + ), + ] diff --git a/backend/backend_trama/models/db.py b/backend/backend_trama/models/db.py index daebd67..ec3a274 100644 --- a/backend/backend_trama/models/db.py +++ b/backend/backend_trama/models/db.py @@ -35,14 +35,14 @@ class Location(models.Model): country = models.CharField(null=True) class Meta: - db_table_comment = "Pre-defined locations configured per-organization" + db_table_comment = "Pre-defined locations configured per-unit" class Course(models.Model): title = models.CharField() description = models.TextField() long_description = models.TextField() - organisation = models.ForeignKey("Organisation", on_delete=models.CASCADE) + unit = models.ForeignKey("Unit", on_delete=models.CASCADE) keywords = models.TextField() level = models.CharField(choices=LEVEL_CHOICES) learning_outcomes = models.TextField() @@ -57,7 +57,7 @@ class CourseSession(models.Model): level = models.CharField(choices=LEVEL_CHOICES) learning_outcomes = models.TextField() contact_email = models.EmailField() - organisation = models.ForeignKey("Organisation", on_delete=models.CASCADE) + unit = models.ForeignKey("Unit", on_delete=models.CASCADE) trainers = models.ManyToManyField(User, blank=True) title = models.CharField() start = models.DateTimeField() @@ -141,7 +141,7 @@ class Theme(models.Model): # pass -class Organisation(models.Model): +class Unit(models.Model): name = models.CharField() contact_email = models.CharField() webaddress = models.CharField() @@ -150,11 +150,11 @@ class Organisation(models.Model): class CertificateTemplate(models.Model): - organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) + unit = models.ForeignKey(Unit, on_delete=models.CASCADE) class FeedbackTemplate(models.Model): - organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) + unit = models.ForeignKey(Unit, on_delete=models.CASCADE) # class User(models.Model): # User already in use, name it something else? diff --git a/backend/tests/test_api_tasks.py b/backend/tests/test_api_tasks.py index 3bb48b2..b4d21da 100644 --- a/backend/tests/test_api_tasks.py +++ b/backend/tests/test_api_tasks.py @@ -4,9 +4,9 @@ from backend_trama.models.db import ( Course, CourseSession, Location, - Organisation, Task, Theme, + Unit, ) from django.contrib.auth.models import User from django.test import TestCase @@ -28,7 +28,7 @@ class TaskListTest(TestCase): self.theme = Theme.objects.create(name="Default Theme", primary_color="#000000") - self.organisation = Organisation.objects.create( + self.unit = Unit.objects.create( name="Test Org", contact_email="org@test.com", webaddress="https://testorg.com", @@ -40,7 +40,7 @@ class TaskListTest(TestCase): title="Test Course", description="Course Description", long_description="Long Course Description", - organisation=self.organisation, + unit=self.unit, keywords="test, course", level="B", learning_outcomes="Learn testing", @@ -59,7 +59,7 @@ class TaskListTest(TestCase): level="B", learning_outcomes="Session learning outcomes", contact_email="contact@testsession.com", - organisation=self.organisation, + unit=self.unit, title="Session Title", start=timezone.now(), end=timezone.now() + timedelta(days=2), diff --git a/backend/tests/test_api_validate_questions.py b/backend/tests/test_api_validate_questions.py index 242f82a..e2ce1ad 100644 --- a/backend/tests/test_api_validate_questions.py +++ b/backend/tests/test_api_validate_questions.py @@ -7,7 +7,7 @@ from trama.api.questions import router # Payload fields: Question, type, options, required # Different type of questions: single, multi, text, yesno # TODO: A type of question that allows file-uploads -# TODO: Mandatory questions (from organisation point of view) +# TODO: Mandatory questions (from unit point of view) # TODO: Sensitive questions (dietary info) # TODO: Conditional questions (dependent on previous answers) diff --git a/backend/trama/api/__init__.py b/backend/trama/api/__init__.py index 8549bed..078c949 100644 --- a/backend/trama/api/__init__.py +++ b/backend/trama/api/__init__.py @@ -10,9 +10,9 @@ from .auth import router as auth_router from .core import router as core_router from .courses import router as courses_router from .locations import router as locations_router -from .organisations import router as organisations_router from .questions import router as questions_router from .tasks import router as tasks_router +from .units import router as units_router from .users import router as users_router api = NinjaExtraAPI(auth=JWTAuth()) @@ -21,7 +21,7 @@ api.register_controllers(NinjaJWTDefaultController) api.add_router("/", core_router, tags=["core"]) api.add_router("/auth/", auth_router, tags=["auth"]) api.add_router("/courses/", courses_router, tags=["courses"]) -api.add_router("/organisations/", organisations_router, tags=["organisations"]) +api.add_router("/units/", units_router, tags=["units"]) api.add_router("/tasks/", tasks_router, tags=["tasks"]) api.add_router("/users/", users_router, tags=["users"]) api.add_router("/locations/", locations_router, tags=["locations"]) diff --git a/backend/trama/api/courses.py b/backend/trama/api/courses.py index 86c717f..c1a132d 100644 --- a/backend/trama/api/courses.py +++ b/backend/trama/api/courses.py @@ -1,6 +1,6 @@ from collections import defaultdict -from backend_trama.models.db import ( +from backend_trama.models.db import ( # CourseQuestion, Course, CourseSession, Location, @@ -39,8 +39,9 @@ def list_courses(request): @router.post("/") def create_course(request, data: CourseSchema): data_dict = data.dict() - # rename the key to avoid the database lookup of the organisation by id - data_dict["organisation_id"] = data_dict.pop("organisation") + + # rename the key to avoid the database lookup of the unit by id + data_dict["unit_id"] = data_dict.pop("unit") new_course = Course.objects.create(**data_dict) return {"id": new_course.id} @@ -51,18 +52,18 @@ def list_course_sessions(request): @router.get("/sessions/upcoming/", response=list[CourseSessionSchema]) -def list_upcoming_course_sessions(request, organisation_id: int | None = None): +def list_upcoming_course_sessions(request, unit_id: int | None = None): queryset = CourseSession.objects.filter(start__gte=timezone.now()) - if organisation_id: - queryset = queryset.filter(organisation_id=organisation_id) + if unit_id: + queryset = queryset.filter(unit_id=unit_id) return queryset @router.get("/sessions/previous/", response=list[CourseSessionSchema]) -def list_previous_course_sessions(request, organisation_id: int | None = None): +def list_previous_course_sessions(request, unit_id: int | None = None): queryset = CourseSession.objects.filter(start__lt=timezone.now()) - if organisation_id: - queryset = queryset.filter(organisation_id=organisation_id) + if unit_id: + queryset = queryset.filter(unit_id=unit_id) return queryset @@ -88,11 +89,11 @@ def create_course_session(request, data: CourseSessionCreateSchema): @router.get("/sessions/upcoming_dates/") -def list_upcoming_session_dates(request, organisation_id: int | None = None): +def list_upcoming_session_dates(request, unit_id: int | None = None): date_dict = nested_dict() course_sessions = CourseSession.objects.filter(start__gte=timezone.now()) - if organisation_id: - course_sessions = course_sessions.filter(organisation_id=organisation_id) + if unit_id: + course_sessions = course_sessions.filter(unit_id=unit_id) for course_session in course_sessions: date_events = [ (course_session.start, "Course starts"), diff --git a/backend/trama/api/organisations.py b/backend/trama/api/organisations.py deleted file mode 100644 index b5c43ad..0000000 --- a/backend/trama/api/organisations.py +++ /dev/null @@ -1,28 +0,0 @@ -from backend_trama.models.db import Organisation -from django.db import IntegrityError -from ninja import Router -from ninja.errors import HttpError - -from .schemas import OrganisationSchema - -router = Router() - - -@router.get("/", response=list[OrganisationSchema]) -def list_organisations(request): - return Organisation.objects.all() - - -@router.post("/") -def create_organisation(request, data: OrganisationSchema): - data_dict = data.dict() - # rename the key to avoid the database lookup of the theme by id - data_dict["theme_id"] = data_dict.pop("theme") - try: - new_organisation = Organisation.objects.create(**data.dict()) - except IntegrityError: - raise HttpError(400, "An organisation with this data already exists.") - except Exception as e: - raise HttpError(500, f"An unexpected error occurred: {str(e)}") - - return {"id": new_organisation.id} diff --git a/backend/trama/api/schemas.py b/backend/trama/api/schemas.py index c41ea85..56488a1 100644 --- a/backend/trama/api/schemas.py +++ b/backend/trama/api/schemas.py @@ -4,11 +4,11 @@ from backend_trama.models.db import ( Course, CourseSession, Location, - Organisation, SessionQuestion, Task, TaskHistory, Theme, + Unit, ) from django.contrib.auth.models import User from ninja import ModelSchema, Schema @@ -46,9 +46,9 @@ class LocationSchema(ModelSchema): fields = "__all__" -class OrganisationSchema(ModelSchema): +class UnitSchema(ModelSchema): class Meta: - model = Organisation + model = Unit fields = "__all__" @@ -145,7 +145,7 @@ class RegistrationResponseSchema(Schema): class CourseSessionCreateSchema(Schema): course_id: int - organisation_id: int + unit_id: int trainers: list[int] location: list[int] keywords: str diff --git a/backend/trama/api/units.py b/backend/trama/api/units.py new file mode 100644 index 0000000..e10bf73 --- /dev/null +++ b/backend/trama/api/units.py @@ -0,0 +1,28 @@ +from backend_trama.models.db import Unit +from django.db import IntegrityError +from ninja import Router +from ninja.errors import HttpError + +from .schemas import UnitSchema + +router = Router() + + +@router.get("/", response=list[UnitSchema]) +def list_units(request): + return Unit.objects.all() + + +@router.post("/") +def create_unit(request, data: UnitSchema): + data_dict = data.dict() + # rename the key to avoid the database lookup of the theme by id + data_dict["theme_id"] = data_dict.pop("theme") + try: + new_unit = Unit.objects.create(**data.dict()) + except IntegrityError: + raise HttpError(400, "An unit with this data already exists.") + except Exception as e: + raise HttpError(500, f"An unexpected error occurred: {str(e)}") + + return {"id": new_unit.id} diff --git a/docs/events/2025-08-06-2nd-LEA-hackathon.md b/docs/events/2025-08-06-2nd-LEA-hackathon.md index e63d908..830ac51 100644 --- a/docs/events/2025-08-06-2nd-LEA-hackathon.md +++ b/docs/events/2025-08-06-2nd-LEA-hackathon.md @@ -28,7 +28,7 @@ - Course flavours (see course discussion document) - We would like to implement this feature but how we would represent the branching from another Course needs a clearer proposal -![Flavour](assets/Flavour.png) +![Flavour](static/Flavour.png) ### Decisions diff --git a/docs/events/assets/Flavour.png b/docs/events/static/Flavour.png similarity index 100% rename from docs/events/assets/Flavour.png rename to docs/events/static/Flavour.png diff --git a/docs/tech/glossary.md b/docs/glossary.md similarity index 75% rename from docs/tech/glossary.md rename to docs/glossary.md index a4fa14b..0f12f46 100644 --- a/docs/tech/glossary.md +++ b/docs/glossary.md @@ -12,7 +12,7 @@ | Instructor | predefined Role in a Session. Permissions: see [User Permissions](user_permissions.md) | | Interests List :question: | A user can express interest in a Course. Once a Session (or its Flavour Course) is created, the user is notified by email.| | Mandatory Course Questions | Questions linked to a Course that appear automatically when a user registers for a Session. They don’t need to be set per Session.| -| Organisation Coordinator :question: Admin | predefined Role :warning: needs to be clarified. Permissions: see [User Permissions](user_permissions.md) | +| Unit Coordinator :question: Admin | predefined Role :warning: needs to be clarified. Permissions: see [User Permissions](user_permissions.md) | | Question Set Editor | Allows a :question: Course Coordinator or Admin to create, edit, and delete Question Sets and assign mandatory sets to Courses. | | QuestionSet | A reusable set of template Questions, currently limited to registration Questions but may include more types in future. | | Session | _Always_ corresponding to a Course. The actual conducted event instance of a Course at a specific time. | @@ -24,14 +24,8 @@ | Term | Definition | |--------------------------|--------------------------------| -| Organisation | Refers to entities like EMBL or SciLifeLab. Note: Not officially part of the glossary since LEA doesn't support multiple organisations at once. Included here for clarity. :warning: We might have used this term in a different way in recent discussion! This term is defined here for a clear definition in the future. | +| Instance | Refers to entities like EMBL or SciLifeLab. Note: Not officially part of the glossary since LEA doesn't support multiple instances at once. This term is defined here for a clear definition for Future Development discussions. :warning: in an earlier stage, Course Sessions might have been referred to as Course Instances. | +| Organisation | We don't use this term anymore since it has been used with multiple definitions. For clarity: the term Organisation has been used in a way that it was referred to as what we call now a Unit, as well as an Instance. | | Trainer | predefined Role in a Session, currently equal to term _Instructor_. Permissions: see [User Permissions](user_permissions.md) | | TraMA | Training Management Platform, prior name of LEA - Learning Events Assistant | | Version | Alternative term for Flavour: A Course derived from an existing one (e.g. “Python for Beginners” → “Python for Beginners with AI”). Both can run in parallel. | - -**TODO:** - -- Add missing terms -- Discuss open Questions → :question: -- Order alphabetically :white_check_mark: -- Possible implementation of [tooltips](https://squidfunk.github.io/mkdocs-material/reference/tooltips/) for displaying the definition while pointing at a term from the glossary in the Documentation diff --git a/docs/in_progress.md b/docs/in_progress.md index 51dda88..28bb28a 100644 --- a/docs/in_progress.md +++ b/docs/in_progress.md @@ -7,8 +7,11 @@ ## Technical documents (TD) - [User centric flow for course registration](tech/user_registration.md) -- [Glossary](tech/glossary.md) ## Activities / Events - ... + +## Glossary + +- [Glossary](glossary.md) diff --git a/docs/product_docs/history_files.md b/docs/product_docs/history_files.md index 326f318..d78acab 100644 --- a/docs/product_docs/history_files.md +++ b/docs/product_docs/history_files.md @@ -2,7 +2,7 @@ ## Links -### Goodle Docs (in Use) +### Google Docs (in Use) You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Since August 2025, the [PrimeVue Mockups](https://docs.google.com/presentation/d/1WsTwzoN9RQ4xwbgO-Ge6kpLYXH1v4kkE/edit?slide=id.p1#slide=id.p1) by Kristen are in use and open for comments or discussions during the weekly meetings. diff --git a/docs/tech/user_permissions.md b/docs/tech/user_permissions.md index 98a9514..6e13d76 100644 --- a/docs/tech/user_permissions.md +++ b/docs/tech/user_permissions.md @@ -7,7 +7,7 @@ The permission system in LEA is inherently hierarchical. -Several roles have been made clear but different entities have expressed needing different organisational roles. +Several roles have been made clear but different entities have expressed needing different Unit roles. Accommodating for role flexibility is a difficult use-case so some compromises are proposed in this document. One of the compromises is to provide pre-defined roles at the time of deployment. @@ -27,8 +27,8 @@ in the list below: Roles: - **System Admin** - Super-user access with all permissions, including access to backend admin interface. -- **Organisation Admin** - :warning: To be clarified -- **Organisation Coordinator** - :warning: To be clarified +- **Unit Admin** - :warning: To be clarified +- **Unit Coordinator** - :warning: To be clarified - **Course Coordinator** - Responsible for the overview and portfolio of Courses. Creates new courses and assigns them to Course Managers. - **Course Manager** - Also known as Organizer, handles logistical aspects about organising a *Course* effectively creating *Sessions*. - **Instructor** - Delivers part of the course material and may be involved in its conceptualization. @@ -44,7 +44,7 @@ This requires roles to be granted at different levels. | Level | |--------------| | System-wide | -| Organisation | +| Unit | | Course | | Session | @@ -62,8 +62,8 @@ These levels will be linked to roles so as to indicate to which level or objects | Role | Level | |--------------------------|--------------------------------| | System Admin | System-wide | -| Organisation Admin | All Organisations :question: | -| Organisation Coordinator | Single Organisation :question: | +| Unit Admin | All Units :question: | +| Unit Coordinator | Single Unit :question: | | Course Coordinator | All Courses | | Course Manager | Single Course / All Sessions | | Instructor | Single Session | @@ -71,10 +71,10 @@ These levels will be linked to roles so as to indicate to which level or objects Practical implications and examples: -- *Course Coordinator* of one organisation is not allowed to edit *Courses* from other organisations. +- *Course Coordinator* of one Unit is not allowed to edit *Courses* from other Units. - *Instructor* in one *Session* has no permissions to edit or view other *Sessions*. - *Course Manager* is only allowed to modify *Courses* they own, not owned by other *Course Managers* -- *Course Coordinator* is allowed to view an dmodify all *Courses* in their organisation. +- *Course Coordinator* is allowed to view an dmodify all *Courses* in their Unit. !!! note @@ -88,15 +88,15 @@ We will follow the core of a **C**reate / **R**ead (or View) / **U**pdate (or Ed A partial permission table then looks as follows: -| Action | Helper | Instructor | Course Manager | Course Coordinator | Organisation Coordinator | Organisation Admin | System Admin | +| Action | Helper | Instructor | Course Manager | Course Coordinator | Unit Coordinator | Unit Admin | System Admin | |------------------------------------------------------------|--------|------------|----------------|--------------------|--------------------------|--------------------|--------------| -| Course Create (under Organisation) | | | | ✓ | ✓ | ✓ | ✓ | -| Course View - Owned/Member-of (drafts under Organisation) | | | ✓ | ✓ | ✓ | ✓ | ✓ | -| Course View - All (drafts under Organisation) | | | | ✓ | ✓ | ✓ | ✓ | -| Course Update - Owned/Member-of (under Organisation) | | | ✓ | ✓ | ✓ | ✓ | ✓ | -| Course Update - All (in Organisation) | | | | ✓ | ✓ | ✓ | ✓ | -| Course Delete - Member-of (in Organisation) | | | | ✓ | ✓ | ✓ | ✓ | -| Course Delete - All (in Organisation) | | | | ✓ | ✓ | ✓ | ✓ | +| Course Create (under Unit) | | | | ✓ | ✓ | ✓ | ✓ | +| Course View - Owned/Member-of (drafts under Unit) | | | ✓ | ✓ | ✓ | ✓ | ✓ | +| Course View - All (drafts under Unit) | | | | ✓ | ✓ | ✓ | ✓ | +| Course Update - Owned/Member-of (under Unit) | | | ✓ | ✓ | ✓ | ✓ | ✓ | +| Course Update - All (in Unit) | | | | ✓ | ✓ | ✓ | ✓ | +| Course Delete - Member-of (in Unit) | | | | ✓ | ✓ | ✓ | ✓ | +| Course Delete - All (in Unit) | | | | ✓ | ✓ | ✓ | ✓ | | Session Create (under Course) | | | ✓ | ✓ | ✓ | ✓ | ✓ | | Session View - Owned/Member-of | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Session View - All | | | ✓ | ✓ | ✓ | ✓ | ✓ | @@ -119,7 +119,7 @@ A partial permission table then looks as follows: This table is currently under construction and not yet final. Permissions in this table are defined at table level, not yet role level. -| Task | Course Manager | Course Coordinator | Organisation Coordinator | Organisation Admin | SuperAdmin | +| Task | Course Manager | Course Coordinator | Unit Coordinator | Unit Admin | SuperAdmin | |---------------------------------------|----------------|--------------------|--------------------------|--------------------|------------| | Course:
Create | | | ✓ | ✓ | ✓ | | Course:
Update | | | ✓ | ✓ | ✓ | @@ -127,16 +127,16 @@ A partial permission table then looks as follows: | CourseSession:
Create | | ✓ | ✓ | ✓ | ✓ | | CourseSession:
Update | ✓ | ✓ | ✓ | ✓ | ✓ | | CourseSession:
Delete | | ✓ | ✓ | ✓ | ✓ | -| Organisation:
Create | | | | | ✓ | -| Organisation:
Update | | | | ✓ | ✓ | -| Organisation:
Delete | | | | | ✓ | +| Unit:
Create | | | | | ✓ | +| Unit:
Update | | | | ✓ | ✓ | +| Unit:
Delete | | | | | ✓ | | Registrations:
Create | | ✓ | ✓ | ✓ | ✓ | | Registration:
Update | ✓ | ✓ | ✓ | ✓ | ✓ | | Registration:
Delete | ✓ | ✓ | ✓ | ✓ | ✓ | | SuperAdmin:
View all | | | | | ✓ | | SuperAdmin:
Assume identity | | | | | ✓ | -| Organisation:
Assign org. adm. | | | | ✓ | ✓ | -| Organisation:
Assign org. coord. | | | ✓ | ✓ | ✓ | +| Unit:
Assign org. adm. | | | | ✓ | ✓ | +| Unit:
Assign org. coord. | | | ✓ | ✓ | ✓ | | CourseSession:
Assign course coord.| | ✓ | ✓ | ✓ | ✓ | | CourseSession:
Assign course man. | | ✓ | ✓ | ✓ | ✓ | | Feedback:
Create templates | | | ✓ | ✓ | ✓ | diff --git a/docs/tech/user_registration.md b/docs/tech/user_registration.md index a4fe7c0..58059cf 100644 --- a/docs/tech/user_registration.md +++ b/docs/tech/user_registration.md @@ -2,7 +2,7 @@ Legend: -- `CourseView` - lists all Courses under a specific organisation, allows searching/filtering +- `CourseView` - lists all Courses under a specific unit, allows searching/filtering - `CourseSessionView` - lists all upcoming Sessions for a given Course - `CourseSessionDetailView` - shows event details and a link to register (if possible) - `CourseRegistrationView` - allows registration to a specific Course Session diff --git a/frontend/src/api/coursesApi.ts b/frontend/src/api/coursesApi.ts index 7d35a11..2bdde87 100644 --- a/frontend/src/api/coursesApi.ts +++ b/frontend/src/api/coursesApi.ts @@ -4,7 +4,7 @@ export async function registerCourse(courseData: { title: string description: string long_description: string - organisation_id: number + unit_id: number keywords: string level: string learning_outcomes: string @@ -14,30 +14,30 @@ export async function registerCourse(courseData: { return response.data } -export async function getCurrentCourseSessions(organisationId?: number) { - const params: { organisation_id?: number } = {} - if (organisationId) { - params.organisation_id = organisationId +export async function getCurrentCourseSessions(unitId?: number) { + const params: { unit_id?: number } = {} + if (unitId) { + params.unit_id = unitId } const response = await $axios.get('/api/courses/sessions/upcoming/', { params }) return response.data } -export async function getPreviousCourseSessions(organisationId?: number) { - const params: { organisation_id?: number } = {} - if (organisationId) { - params.organisation_id = organisationId +export async function getPreviousCourseSessions(unitId?: number) { + const params: { unit_id?: number } = {} + if (unitId) { + params.unit_id = unitId } const response = await $axios.get('/api/courses/sessions/previous/', { params }) return response.data } -export async function getUpcomingDates(organisationId?: number) { - const params: { organisation_id?: number } = {} - if (organisationId) { - params.organisation_id = organisationId +export async function getUpcomingDates(unitId?: number) { + const params: { unit_id?: number } = {} + if (unitId) { + params.unit_id = unitId } const response = await $axios.get('/api/courses/sessions/upcoming_dates/', { params }) diff --git a/frontend/src/api/organisationsApi.ts b/frontend/src/api/organisationsApi.ts deleted file mode 100644 index 8d69350..0000000 --- a/frontend/src/api/organisationsApi.ts +++ /dev/null @@ -1,6 +0,0 @@ -import $axios from '@/interceptors/axios' - -export async function getOrganisations() { - const response = await $axios.get('/api/organisations/') - return response.data -} diff --git a/frontend/src/api/unitsApi.ts b/frontend/src/api/unitsApi.ts new file mode 100644 index 0000000..0f78d91 --- /dev/null +++ b/frontend/src/api/unitsApi.ts @@ -0,0 +1,6 @@ +import $axios from '@/interceptors/axios' + +export async function getUnits() { + const response = await $axios.get('/api/units/') + return response.data +} diff --git a/frontend/src/components/RegisterCourseForm.vue b/frontend/src/components/RegisterCourseForm.vue index 05ce010..7ce43df 100644 --- a/frontend/src/components/RegisterCourseForm.vue +++ b/frontend/src/components/RegisterCourseForm.vue @@ -2,38 +2,38 @@ import { ref, onMounted } from 'vue' import { useToastHandler } from '@/composables/useToastHandler' import { useRouter } from 'vue-router' -import { getOrganisations } from '@/api/organisationsApi' +import { getUnits } from '@/api/unitsApi' import { registerCourse } from '@/api/coursesApi' const title = ref('') const description = ref('') const longDescription = ref('') -const organisation_id = ref(null) +const unit_id = ref(null) const keywords = ref('') const level = ref('Beginner') const learningOutcomes = ref('') const code = ref('') -const organisationsList = ref<{ value: number; label: string }[]>([]) +const unitsList = ref<{ value: number; label: string }[]>([]) const router = useRouter() const { showToast } = useToastHandler() -interface Organisation { +interface Unit { id: number name: string } onMounted(async () => { try { - const data = await getOrganisations() - organisationsList.value = (data as Organisation[]).map((org) => ({ + const data = await getUnits() + unitsList.value = (data as Unit[]).map((org) => ({ value: org.id, label: org.name })) } catch (error) { showToast({ severity: 'error', - detail: (error as Error)?.message || 'Failed to fetch organisations.' + detail: (error as Error)?.message || 'Failed to fetch units.' }) } }) @@ -44,7 +44,7 @@ async function handleRegisterCourse() { title: title.value, description: description.value, long_description: longDescription.value, - organisation_id: Number(organisation_id.value), + unit_id: Number(unit_id.value), keywords: keywords.value, level: level.value, learning_outcomes: learningOutcomes.value, @@ -98,11 +98,11 @@ async function handleRegisterCourse() { /> (null) +const unit_id = ref(null) const courseTitle = ref('') const start = ref('') const end = ref('') @@ -55,7 +55,7 @@ const registrationDeadline = ref('') const confirmationDate = ref('') const attendanceThreshold = ref(80) const trainerIds = ref([]) -const organisationsList = ref<{ value: number; label: string }[]>([]) +const unitsList = ref<{ value: number; label: string }[]>([]) interface Question { question: string @@ -101,9 +101,9 @@ onMounted(async () => { label: loc.name })) - // Load organisations - const responseOrganisations = await $axios.get(`/api/organisations/`) - organisationsList.value = (responseOrganisations.data as Organisation[]).map((org) => ({ + // Load units + const responseUnits = await $axios.get(`/api/units/`) + unitsList.value = (responseUnits.data as Unit[]).map((org) => ({ value: org.id, label: org.name })) @@ -204,7 +204,7 @@ async function registerCourseSession() { level: level.value, learning_outcomes: learningOutcomes.value, contact_email: contactEmail.value, - organisation_id: Number(organisation_id.value), + unit_id: Number(unit_id.value), title: courseTitle.value, start: toISOStringWithTimezone(start.value), end: toISOStringWithTimezone(end.value), @@ -288,11 +288,11 @@ async function registerCourseSession() { validation="required|email" />

- {{ course.organisation_name || 'Unknown Organization' }} + {{ course.unit_name || 'Unknown Unit' }}

diff --git a/frontend/src/views/CourseView.vue b/frontend/src/views/CourseView.vue index 03783dd..9552e0d 100644 --- a/frontend/src/views/CourseView.vue +++ b/frontend/src/views/CourseView.vue @@ -76,13 +76,7 @@ onMounted(async () => {

Organizing Institution: - {{ - course?.organisation === 1 - ? 'NBIS' - : course?.organisation === 2 - ? 'EMBL' - : 'Unknown Organisation' - }} + {{ course?.unit === 1 ? 'NBIS' : course?.unit === 2 ? 'EMBL' : 'Unknown Unit' }}

Level: {{ course?.level || 'No level available.' }}

diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 2102696..5994c54 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -16,7 +16,7 @@ import { RouterLink } from 'vue-router' Register Course

  • - + Register Course Session
  • diff --git a/frontend/src/views/RegisterCourseInstanceView.vue b/frontend/src/views/RegisterCourseInstanceView.vue deleted file mode 100644 index a169526..0000000 --- a/frontend/src/views/RegisterCourseInstanceView.vue +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/frontend/src/views/RegisterCourseSessionView.vue b/frontend/src/views/RegisterCourseSessionView.vue new file mode 100644 index 0000000..70d2aaa --- /dev/null +++ b/frontend/src/views/RegisterCourseSessionView.vue @@ -0,0 +1,7 @@ + + diff --git a/frontend/src/views/admin/ViewCourseSessionsView.vue b/frontend/src/views/admin/ViewCourseSessionsView.vue index 7a0481c..eefe525 100644 --- a/frontend/src/views/admin/ViewCourseSessionsView.vue +++ b/frontend/src/views/admin/ViewCourseSessionsView.vue @@ -7,7 +7,7 @@ import { getPreviousCourseSessions, getUpcomingDates } from '@/api/coursesApi' -import { getOrganisations } from '@/api/organisationsApi' +import { getUnits } from '@/api/unitsApi' import { useDialog } from 'primevue/usedialog' import CourseCreateModal from '@/components/model/CourseCreateModal.vue' const toast = useToast() @@ -21,8 +21,8 @@ const currentCourseSessions = ref([]) const previousCourseSessions = ref([]) const years = ref([currentYear, currentYear - 1, currentYear - 2]) const selectedYear = ref(currentYear) -const organisations = ref([]) -const selectedOrganisationId = ref(null) +const units = ref([]) +const selectedUnitId = ref(null) const upcomingDates = ref([]) // methods @@ -47,7 +47,7 @@ watch(search, (val) => { onMounted(() => { fetchCurrentCourseSessions() fetchPreviousCourseSessions() - fetchOrganisations() + fetchUnits() fetchUpcomingDates() }) @@ -57,9 +57,7 @@ onMounted(() => { async function fetchCurrentCourseSessions() { try { - currentCourseSessions.value = await getCurrentCourseSessions( - selectedOrganisationId.value || undefined - ) + currentCourseSessions.value = await getCurrentCourseSessions(selectedUnitId.value || undefined) } catch { toast.add({ severity: 'error', @@ -73,7 +71,7 @@ async function fetchCurrentCourseSessions() { async function fetchPreviousCourseSessions() { try { previousCourseSessions.value = await getPreviousCourseSessions( - selectedOrganisationId.value || undefined + selectedUnitId.value || undefined ) } catch { toast.add({ @@ -85,14 +83,14 @@ async function fetchPreviousCourseSessions() { } } -async function fetchOrganisations() { +async function fetchUnits() { try { - organisations.value = await getOrganisations() + units.value = await getUnits() } catch { toast.add({ severity: 'error', - summary: 'Fetch Organisations Failed', - detail: 'Failed to fetch organisations.', + summary: 'Fetch Units Failed', + detail: 'Failed to fetch Units.', life: 5000 }) } @@ -100,7 +98,7 @@ async function fetchOrganisations() { async function fetchUpcomingDates() { try { - upcomingDates.value = await getUpcomingDates(selectedOrganisationId.value || undefined) + upcomingDates.value = await getUpcomingDates(selectedUnitId.value || undefined) } catch { toast.add({ severity: 'error', @@ -144,13 +142,13 @@ function handleOrganizationChangeSelect() { Welcome back <instructorname>
    - + -- GitLab From 81a0cb8750ff5e5ca39fc669dec680794b785603 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Wed, 13 Aug 2025 12:58:38 +0000 Subject: [PATCH 08/32] Frontend: fixed typo in Signup Form --- frontend/src/components/auth/SignupForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/auth/SignupForm.vue b/frontend/src/components/auth/SignupForm.vue index f9277b3..80b8d00 100644 --- a/frontend/src/components/auth/SignupForm.vue +++ b/frontend/src/components/auth/SignupForm.vue @@ -25,7 +25,7 @@ async function registerUser() { showToast({ severity: 'success', - summary: 'Registration successful! refirecting to homepage' + summary: 'Registration successful! Redirecting to homepage' }) router.push({ name: 'HomeView' }) } catch (error) { -- GitLab From 435327c236b9c3c4b9a2fb528c5fe8e79a34b195 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Wed, 13 Aug 2025 13:07:39 +0000 Subject: [PATCH 09/32] Docs: History_files update link to current mockups --- docs/product_docs/history_files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product_docs/history_files.md b/docs/product_docs/history_files.md index d78acab..4560a42 100644 --- a/docs/product_docs/history_files.md +++ b/docs/product_docs/history_files.md @@ -4,7 +4,7 @@ ### Google Docs (in Use) -You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Since August 2025, the [PrimeVue Mockups](https://docs.google.com/presentation/d/1WsTwzoN9RQ4xwbgO-Ge6kpLYXH1v4kkE/edit?slide=id.p1#slide=id.p1) by Kristen are in use and open for comments or discussions during the weekly meetings. +You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Since August 2025, the [PrimeVue Mockups](https://docs.google.com/presentation/d/1eUz8xKdn_HyNGeJlMOGNlOMmi3ukUTA9/) by Kristen are in use and open for comments or discussions during the weekly meetings. Additionally, the Docs includes: -- GitLab From 8a5e2dad31c44ff5ccd27dbdfdab635a24a868b9 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 07:53:59 +0000 Subject: [PATCH 10/32] frontend: fixed display of Unit (removed hard-coded EMBL/NBIS) --- backend/trama/api/schemas.py | 14 ++++++++------ frontend/src/views/CourseListView.vue | 2 +- frontend/src/views/CourseView.vue | 8 ++++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/trama/api/schemas.py b/backend/trama/api/schemas.py index 56488a1..df82cfa 100644 --- a/backend/trama/api/schemas.py +++ b/backend/trama/api/schemas.py @@ -15,7 +15,15 @@ from ninja import ModelSchema, Schema from pydantic import EmailStr, Field +class UnitSchema(ModelSchema): + class Meta: + model = Unit + fields = "__all__" + + class CourseSchema(ModelSchema): + unit: UnitSchema + class Meta: model = Course fields = "__all__" @@ -46,12 +54,6 @@ class LocationSchema(ModelSchema): fields = "__all__" -class UnitSchema(ModelSchema): - class Meta: - model = Unit - fields = "__all__" - - class ThemeSchema(ModelSchema): class Meta: model = Theme diff --git a/frontend/src/views/CourseListView.vue b/frontend/src/views/CourseListView.vue index f4cc774..7a5b8fb 100644 --- a/frontend/src/views/CourseListView.vue +++ b/frontend/src/views/CourseListView.vue @@ -131,7 +131,7 @@ function getImage(i) { {{ course.title }}

    - {{ course.unit_name || 'Unknown Unit' }} + {{ course?.unit?.name || 'Unknown Unit' }}

    diff --git a/frontend/src/views/CourseView.vue b/frontend/src/views/CourseView.vue index 9552e0d..c994842 100644 --- a/frontend/src/views/CourseView.vue +++ b/frontend/src/views/CourseView.vue @@ -73,11 +73,15 @@ onMounted(async () => { {{ course?.long_description || 'No long description available for this course.' }}
    +
    + Organizing Unit: + {{ course?.unit?.name || 'Unknown Unit' }} +
    -

    +

    Level: {{ course?.level || 'No level available.' }}

    Learning outcomes: {{ course?.learning_outcomes || 'No learning outcomes available.' }} -- GitLab From d727667e7f85700bd4d3d77800838f1862f8d448 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 07:55:24 +0000 Subject: [PATCH 11/32] changed term organization -> unit --- frontend/src/views/CourseListView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/CourseListView.vue b/frontend/src/views/CourseListView.vue index 7a5b8fb..046de8b 100644 --- a/frontend/src/views/CourseListView.vue +++ b/frontend/src/views/CourseListView.vue @@ -70,9 +70,9 @@ function getImage(i) { />

    - +
    -- GitLab From 31e53d7d034c2a0a2cd6c213423c4b4b4c21dee3 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 08:15:47 +0000 Subject: [PATCH 12/32] backend: setup for mandatory SystemQuestionSets --- backend/backend_trama/models/db.py | 18 ++++++++++ backend/trama/api/courses.py | 58 +++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/backend/backend_trama/models/db.py b/backend/backend_trama/models/db.py index ec3a274..cd0a232 100644 --- a/backend/backend_trama/models/db.py +++ b/backend/backend_trama/models/db.py @@ -114,6 +114,24 @@ class TemplateQuestion(models.Model): required = models.BooleanField(default=False) +class SystemQuestionSet(models.Model): + name = models.CharField(unique=True) + question_type = models.CharField(choices=QUESTION_TYPES) + + +class SystemQuestion(models.Model): + set = models.ForeignKey( + SystemQuestionSet, on_delete=models.CASCADE, related_name="system_questions" + ) + question = models.CharField(max_length=500) + type = models.CharField( + max_length=6, + choices=ANSWER_QUESTION_CHOICES, + ) + options = models.JSONField() + required = models.BooleanField(default=False) + + class Registration(TimestampModel): course_session = models.ForeignKey(CourseSession, on_delete=models.CASCADE) participant = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/backend/trama/api/courses.py b/backend/trama/api/courses.py index c1a132d..0b5f685 100644 --- a/backend/trama/api/courses.py +++ b/backend/trama/api/courses.py @@ -6,6 +6,8 @@ from backend_trama.models.db import ( # CourseQuestion, Location, Registration, SessionQuestion, + SystemQuestion, + SystemQuestionSet, TemplateQuestion, TemplateQuestionSet, ) @@ -33,7 +35,7 @@ router = Router() @router.get("/", response=list[CourseSchema]) def list_courses(request): - return Course.objects.all() + return Course.objects.select_related("unit").all() @router.post("/") @@ -223,3 +225,57 @@ def list_session_registrations(request, session_id: int): for reg in registrations ], } + + +# System-wide mandatory questions + + +@router.get("/system-question-sets/", response=list[TemplateQuestionSetSchema]) +def list_system_question_sets(request): + return SystemQuestionSet.objects.prefetch_related("system_questions").all() + + +@router.post("/system-question-sets/", response=TemplateQuestionSetSchema) +def create_system_question_set(request, data: TemplateQuestionSetSchema): + if SystemQuestionSet.objects.filter(name=data.name).exists(): + raise HttpError(400, "Question Set with this name already exists") + + new_set = SystemQuestionSet.objects.create(name=data.name) + + for q in data.questions: + SystemQuestion.objects.create(set=new_set, **q.dict()) + + return TemplateQuestionSetSchema( + id=new_set.id, name=new_set.name, questions=data.questions + ) + + +@router.put("/system-question-sets/{id}", response=TemplateQuestionSetSchema) +def update_system_question_set(request, id: int, data: TemplateQuestionSetUpdateSchema): + qs = get_object_or_404(SystemQuestionSet, id=id) + + if SystemQuestionSet.objects.exclude(id=id).filter(name=data.name).exists(): + raise ValidationError( + [("name", "A Question Set with this name already exists.")] + ) + + qs.questions.all().delete() + + for q in data.questions: + SystemQuestion.objects.create(set=qs, **q.dict()) + + qs.name = data.name + qs.save() + + return TemplateQuestionSetSchema( + id=qs.id, + name=qs.name, + questions=data.questions, + ) + + +@router.delete("/system-question-sets/{id}") +def delete_system_question_set(request, id: str): + qs = get_object_or_404(SystemQuestionSet, id=id) + qs.delete() + return {"message": f"Set '{id}' deleted successfully"} -- GitLab From 81c7660d7556dec87e7b2987e43f59502e9800a3 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 08:19:41 +0000 Subject: [PATCH 13/32] updated link to Mockup slides --- docs/product_docs/history_files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product_docs/history_files.md b/docs/product_docs/history_files.md index a46135c..430f51d 100644 --- a/docs/product_docs/history_files.md +++ b/docs/product_docs/history_files.md @@ -4,7 +4,7 @@ ### Google Docs (in Use) -You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Since August 2025, the [PrimeVue Mockups](https://docs.google.com/presentation/d/1WsTwzoN9RQ4xwbgO-Ge6kpLYXH1v4kkE/edit?slide=id.p1#slide=id.p1) by Kristen are in use and open for comments or discussions during the weekly meetings. +You can find the link to the Google Docs [here](https://drive.google.com/drive/folders/12JCFL1zktqglaKEbd48JBIV5hgLXhKtf). Since August 2025, the [PrimeVue Mockups](https://docs.google.com/presentation/d/1eUz8xKdn_HyNGeJlMOGNlOMmi3ukUTA9/) by Kristen are in use and open for comments or discussions during the weekly meetings. Additionally, the Docs includes: -- GitLab From dba489344df71528c0fb82367657ce88ca76f12e Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 09:57:50 +0000 Subject: [PATCH 14/32] Frontend: Validate Questions in QuestionSetEditor --- frontend/src/components/QuestionSetEditor.vue | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/QuestionSetEditor.vue b/frontend/src/components/QuestionSetEditor.vue index 5017e69..3d5f729 100644 --- a/frontend/src/components/QuestionSetEditor.vue +++ b/frontend/src/components/QuestionSetEditor.vue @@ -92,6 +92,34 @@ async function saveSet() { questions: editedSet.questions } + // Validate Questions + for (const [qIdx, q] of editedSet.questions.entries()) { + if (!q.question.trim()) { + showToast({ + severity: 'error', + summary: `Question #${qIdx + 1} text cannot be empty.` + }) + return + } + if (q.type === 'single' || q.type === 'multi') { + const emptyOptionIdx = q.options.findIndex((opt) => !opt.trim()) + if (emptyOptionIdx !== -1) { + showToast({ + severity: 'error', + summary: `Option #${emptyOptionIdx + 1} in Question #${qIdx + 1} cannot be empty.` + }) + return + } + if (q.options.length === 0) { + showToast({ + severity: 'error', + summary: `Question #${qIdx + 1} must have at least one option.` + }) + return + } + } + } + try { let message = '' if (selectedSetIndex.value !== null && selectedSetIndex.value !== -1) { @@ -123,12 +151,12 @@ async function saveSet() { summary: message }) router.push('/') - } catch { - // @adeel: as we discussed, we want to get the error information from the backend - // and make sure the User knows what to do/ if its a server error + } catch (err) { + // Handle server/backend errors + const errorMsg = err.response?.data?.detail || 'An unexpected error occurred.' showToast({ severity: 'error', - summary: 'Failed to load question sets' + summary: errorMsg }) } } -- GitLab From b12c45c10bce782018848e353159446f62394f03 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 11:38:14 +0000 Subject: [PATCH 15/32] Backend: Validate Questions for Session creation and Question Sets --- backend/trama/api/courses.py | 68 +++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/backend/trama/api/courses.py b/backend/trama/api/courses.py index 0b5f685..7ff594a 100644 --- a/backend/trama/api/courses.py +++ b/backend/trama/api/courses.py @@ -1,6 +1,6 @@ from collections import defaultdict -from backend_trama.models.db import ( # CourseQuestion, +from backend_trama.models.db import ( Course, CourseSession, Location, @@ -78,6 +78,25 @@ def create_course_session(request, data: CourseSessionCreateSchema): trainers = data_dict.pop("trainers", []) location = data_dict.pop("location", []) + # Validate questions + for q in questions_data: + # Check filled question field + if not q.get("question") or not q["question"].strip(): + raise HttpError(400, "Question text cannot be empty") + # Check options for single- and multi-select questions + if q.get("type") in ("single", "multi"): + options = q.get("options") or [] + if len(options) == 0: + raise HttpError( + 400, + "Single or multi-choice questions must have at least one option", + ) + + if any(not opt.strip() for opt in options): + raise HttpError( + 400, "Single or multi-choice questions cannot have empty options" + ) + # Create session new_session = CourseSession.objects.create(**data_dict) new_session.trainers.set(User.objects.filter(id__in=trainers)) @@ -121,11 +140,42 @@ def list_question_sets(request): return TemplateQuestionSet.objects.prefetch_related("questions").all() +# @router.post("/question-sets/", response=TemplateQuestionSetSchema) +# def create_question_set(request, data: TemplateQuestionSetSchema): +# if TemplateQuestionSet.objects.filter(name=data.name).exists(): +# raise HttpError(400, "Question Set with this name already exists") + +# new_set = TemplateQuestionSet.objects.create(name=data.name) + +# for q in data.questions: +# TemplateQuestion.objects.create(set=new_set, **q.dict()) + +# return TemplateQuestionSetSchema( +# id=new_set.id, name=new_set.name, questions=data.questions +# ) + + @router.post("/question-sets/", response=TemplateQuestionSetSchema) def create_question_set(request, data: TemplateQuestionSetSchema): if TemplateQuestionSet.objects.filter(name=data.name).exists(): raise HttpError(400, "Question Set with this name already exists") + # Validate questions + for q in data.questions: + if not q.get("question") or not q["question"].strip(): + raise HttpError(400, "Question text cannot be empty") + if q.get("type") in ("single", "multi"): + options = q.get("options") or [] + if len(options) == 0: + raise HttpError( + 400, + "Single or multi-choice questions must have at least one option", + ) + if any(not opt.strip() for opt in options): + raise HttpError( + 400, "Single or multi-choice questions cannot have empty options" + ) + new_set = TemplateQuestionSet.objects.create(name=data.name) for q in data.questions: @@ -147,6 +197,22 @@ def update_question_set(request, id: int, data: TemplateQuestionSetUpdateSchema) qs.questions.all().delete() + # Validate questions + for q in data.questions: + if not q.get("question") or not q["question"].strip(): + raise HttpError(400, "Question text cannot be empty") + if q.get("type") in ("single", "multi"): + options = q.get("options") or [] + if len(options) == 0: + raise HttpError( + 400, + "Single or multi-choice questions must have at least one option", + ) + if any(not opt.strip() for opt in options): + raise HttpError( + 400, "Single or multi-choice questions cannot have empty options" + ) + for q in data.questions: TemplateQuestion.objects.create(set=qs, **q.dict()) -- GitLab From b2aa3e8774bb5da3feb210f3fca8f256f1f2a7fc Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 11:59:12 +0000 Subject: [PATCH 16/32] frontend: System-wide Question Set Editor View --- .../components/SystemQuestionSetEditor.vue | 282 ++++++++++++++++++ frontend/src/router/index.ts | 6 + frontend/src/views/HomeView.vue | 5 + frontend/src/views/SystemQuestionSetView.vue | 8 + 4 files changed, 301 insertions(+) create mode 100644 frontend/src/components/SystemQuestionSetEditor.vue create mode 100644 frontend/src/views/SystemQuestionSetView.vue diff --git a/frontend/src/components/SystemQuestionSetEditor.vue b/frontend/src/components/SystemQuestionSetEditor.vue new file mode 100644 index 0000000..b1c71b9 --- /dev/null +++ b/frontend/src/components/SystemQuestionSetEditor.vue @@ -0,0 +1,282 @@ + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 37fea08..4167aee 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -13,6 +13,7 @@ import SessionView from '../views/SessionView.vue' import QuestionSetView from '../views/QuestionSetView.vue' import CourseRegistrationView from '../views/CourseRegistrationView.vue' import CourseView from '../views/CourseView.vue' +import SystemQuestionSetView from '@/views/SystemQuestionSetView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -77,6 +78,11 @@ const router = createRouter({ path: '/courses/:courseId', name: 'CourseView', component: CourseView + }, + { + path: '/system-question-sets', + name: 'SystemQuestionSets', + component: SystemQuestionSetView } ] }) diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 5994c54..05e6cfc 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -41,6 +41,11 @@ import { RouterLink } from 'vue-router' Manage Question Sets +
  • + + Manage System-Wide Question Sets + +
  • diff --git a/frontend/src/views/SystemQuestionSetView.vue b/frontend/src/views/SystemQuestionSetView.vue new file mode 100644 index 0000000..6bf2de5 --- /dev/null +++ b/frontend/src/views/SystemQuestionSetView.vue @@ -0,0 +1,8 @@ + + + -- GitLab From 7fe45fbce1882a3641a39253602020a834ece187 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 12:00:10 +0000 Subject: [PATCH 17/32] Migration for System Question Set Model --- .../0003_systemquestionset_systemquestion.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 backend/backend_trama/migrations/0003_systemquestionset_systemquestion.py diff --git a/backend/backend_trama/migrations/0003_systemquestionset_systemquestion.py b/backend/backend_trama/migrations/0003_systemquestionset_systemquestion.py new file mode 100644 index 0000000..143c3e9 --- /dev/null +++ b/backend/backend_trama/migrations/0003_systemquestionset_systemquestion.py @@ -0,0 +1,77 @@ +# Generated by Django 5.2.4 on 2025-08-14 11:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend_trama", "0002_rename_organisation_unit_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="SystemQuestionSet", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(unique=True)), + ( + "question_type", + models.CharField( + choices=[ + ("R", "Registration"), + ("P", "Pre-workshop feedback"), + ("O", "Post-workshop feedback"), + ("I", "Impact feedback"), + ] + ), + ), + ], + ), + migrations.CreateModel( + name="SystemQuestion", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("question", models.CharField(max_length=500)), + ( + "type", + models.CharField( + choices=[ + ("single", "Single choice"), + ("multi", "Multiple choice"), + ("text", "Free text"), + ("yesno", "Yes/No"), + ], + max_length=6, + ), + ), + ("options", models.JSONField()), + ("required", models.BooleanField(default=False)), + ( + "set", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="system_questions", + to="backend_trama.systemquestionset", + ), + ), + ], + ), + ] -- GitLab From b841b94a66001383b56dbde47a9a3bf907763cdd Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 14:31:27 +0000 Subject: [PATCH 18/32] fixed API endpoints for System Mandatory Questions --- backend/trama/api/courses.py | 110 ++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/backend/trama/api/courses.py b/backend/trama/api/courses.py index 7ff594a..917b720 100644 --- a/backend/trama/api/courses.py +++ b/backend/trama/api/courses.py @@ -25,6 +25,7 @@ from .schemas import ( RegistrationCreateSchema, RegistrationResponseSchema, SessionQuestionSchema, + TemplateQuestionSchema, TemplateQuestionSetSchema, TemplateQuestionSetUpdateSchema, UserResponseSchema, @@ -140,21 +141,6 @@ def list_question_sets(request): return TemplateQuestionSet.objects.prefetch_related("questions").all() -# @router.post("/question-sets/", response=TemplateQuestionSetSchema) -# def create_question_set(request, data: TemplateQuestionSetSchema): -# if TemplateQuestionSet.objects.filter(name=data.name).exists(): -# raise HttpError(400, "Question Set with this name already exists") - -# new_set = TemplateQuestionSet.objects.create(name=data.name) - -# for q in data.questions: -# TemplateQuestion.objects.create(set=new_set, **q.dict()) - -# return TemplateQuestionSetSchema( -# id=new_set.id, name=new_set.name, questions=data.questions -# ) - - @router.post("/question-sets/", response=TemplateQuestionSetSchema) def create_question_set(request, data: TemplateQuestionSetSchema): if TemplateQuestionSet.objects.filter(name=data.name).exists(): @@ -294,54 +280,84 @@ def list_session_registrations(request, session_id: int): # System-wide mandatory questions +@router.get("/system-question-set/", response=TemplateQuestionSetSchema | None) +def get_system_question_set(request): + qs_set = SystemQuestionSet.objects.prefetch_related("system_questions").first() + if not qs_set: + return None + return TemplateQuestionSetSchema( + id=qs_set.id, + name=qs_set.name, + questions=[ + TemplateQuestionSchema( + question=q.question, type=q.type, options=q.options, required=True + ) + for q in qs_set.system_questions.all() + ], + ) -@router.get("/system-question-sets/", response=list[TemplateQuestionSetSchema]) -def list_system_question_sets(request): - return SystemQuestionSet.objects.prefetch_related("system_questions").all() - - -@router.post("/system-question-sets/", response=TemplateQuestionSetSchema) -def create_system_question_set(request, data: TemplateQuestionSetSchema): - if SystemQuestionSet.objects.filter(name=data.name).exists(): - raise HttpError(400, "Question Set with this name already exists") +@router.post("/system-question-set/", response=TemplateQuestionSetSchema) +def create_system_question_set(request, data: TemplateQuestionSetUpdateSchema): + if SystemQuestionSet.objects.exists(): + raise HttpError( + 400, + "Internal Server Error: A system question set already exists but can not be loaded.", + ) new_set = SystemQuestionSet.objects.create(name=data.name) - for q in data.questions: - SystemQuestion.objects.create(set=new_set, **q.dict()) + SystemQuestion.objects.create( + set=new_set, + question=q.question, + type=q.type, + options=q.options, + required=True, + ) return TemplateQuestionSetSchema( - id=new_set.id, name=new_set.name, questions=data.questions + id=new_set.id, + name=new_set.name, + questions=[ + TemplateQuestionSchema( + question=q.question, type=q.type, options=q.options, required=True + ) + for q in new_set.system_questions.all() + ], ) -@router.put("/system-question-sets/{id}", response=TemplateQuestionSetSchema) +@router.put("/system-question-set/{id}", response=TemplateQuestionSetSchema) def update_system_question_set(request, id: int, data: TemplateQuestionSetUpdateSchema): - qs = get_object_or_404(SystemQuestionSet, id=id) + qs_set = get_object_or_404(SystemQuestionSet, id=id) + qs_set.system_questions.all().delete() - if SystemQuestionSet.objects.exclude(id=id).filter(name=data.name).exists(): - raise ValidationError( - [("name", "A Question Set with this name already exists.")] + for q in data.questions: + SystemQuestion.objects.create( + set=qs_set, + question=q.question, + type=q.type, + options=q.options, + required=True, ) - qs.questions.all().delete() - - for q in data.questions: - SystemQuestion.objects.create(set=qs, **q.dict()) - - qs.name = data.name - qs.save() + qs_set.name = data.name + qs_set.save() return TemplateQuestionSetSchema( - id=qs.id, - name=qs.name, - questions=data.questions, + id=qs_set.id, + name=qs_set.name, + questions=[ + TemplateQuestionSchema( + question=q.question, type=q.type, options=q.options, required=True + ) + for q in qs_set.system_questions.all() + ], ) -@router.delete("/system-question-sets/{id}") -def delete_system_question_set(request, id: str): - qs = get_object_or_404(SystemQuestionSet, id=id) - qs.delete() - return {"message": f"Set '{id}' deleted successfully"} +@router.delete("/system-question-set/{id}") +def delete_system_question_set(request, id: int): + qs_set = get_object_or_404(SystemQuestionSet, id=id) + qs_set.delete() + return {"message": f"System Question Set '{id}' deleted successfully"} -- GitLab From 4b49e52990575d2949f7808b8f69b9ff86d3df8c Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Thu, 14 Aug 2025 14:43:51 +0000 Subject: [PATCH 19/32] fixed: System Question Set Editor - only one Set --- .../components/SystemQuestionSetEditor.vue | 186 +++++++----------- 1 file changed, 70 insertions(+), 116 deletions(-) diff --git a/frontend/src/components/SystemQuestionSetEditor.vue b/frontend/src/components/SystemQuestionSetEditor.vue index b1c71b9..56094da 100644 --- a/frontend/src/components/SystemQuestionSetEditor.vue +++ b/frontend/src/components/SystemQuestionSetEditor.vue @@ -2,9 +2,7 @@ import { ref, reactive } from 'vue' import $axios from '@/interceptors/axios' import { useToastHandler } from '@/composables/useToastHandler' -import { useRouter } from 'vue-router' -const router = useRouter() const { showToast } = useToastHandler() interface Question { @@ -15,49 +13,37 @@ interface Question { } interface QuestionSet { - id: number + id: number | null name: string questions: Question[] } -const questionSets = ref([]) -const selectedSetIndex = ref(null) const editedSet = reactive({ - id: 0, + id: null, name: '', questions: [] }) -async function loadQuestionSets() { +const isLoading = ref(false) + +async function loadSystemQuestionSet() { + isLoading.value = true try { - const res = await $axios.get(`/api/courses/system-question-sets/`) - questionSets.value = res.data + const res = await $axios.get(`/api/courses/system-question-set/`) + if (res.data) { + editedSet.id = res.data.id + editedSet.name = res.data.name + editedSet.questions = res.data.questions + } else { + // No set exists yet + editedSet.id = null + editedSet.name = '' + editedSet.questions = [] + } } catch { - showToast({ - severity: 'error', - summary: 'Failed to load question sets' - }) - } -} - -function selectSet(index: number) { - if (index === -1) { - selectedSetIndex.value = -1 - editedSet.id = 0 - editedSet.name = '' - editedSet.questions = [] - addQuestion() - } else if (index >= 0) { - selectedSetIndex.value = index - const set = questionSets.value[index] - editedSet.id = set.id - editedSet.name = set.name - editedSet.questions = JSON.parse(JSON.stringify(set.questions)) - } else { - selectedSetIndex.value = null - editedSet.id = 0 - editedSet.name = '' - editedSet.questions = [] + showToast({ severity: 'error', summary: 'Failed to load mandatory system-questions' }) + } finally { + isLoading.value = false } } @@ -66,7 +52,7 @@ function addQuestion() { question: '', type: 'text', options: [], - required: false + required: true }) } @@ -83,12 +69,9 @@ function removeOption(questionIndex: number, optionIndex: number) { } async function saveSet() { - const payload = { - name: editedSet.name, - questions: editedSet.questions - } + editedSet.questions.forEach((q) => (q.required = true)) - // Validate Questions + // Validate question for (const [qIdx, q] of editedSet.questions.entries()) { if (!q.question.trim()) { showToast({ @@ -98,18 +81,18 @@ async function saveSet() { return } if (q.type === 'single' || q.type === 'multi') { - const emptyOptionIdx = q.options.findIndex((opt) => !opt.trim()) - if (emptyOptionIdx !== -1) { + if (q.options.length === 0) { showToast({ severity: 'error', - summary: `Option #${emptyOptionIdx + 1} in Question #${qIdx + 1} cannot be empty.` + summary: `Question #${qIdx + 1} must have at least one option.` }) return } - if (q.options.length === 0) { + const emptyOpt = q.options.findIndex((opt) => !opt.trim()) + if (emptyOpt !== -1) { showToast({ severity: 'error', - summary: `Question #${qIdx + 1} must have at least one option.` + summary: `Option #${emptyOpt + 1} in Question #${qIdx + 1} cannot be empty.` }) return } @@ -117,89 +100,54 @@ async function saveSet() { } try { - let message = '' - if (selectedSetIndex.value !== null && selectedSetIndex.value !== -1) { - const originalId = questionSets.value[selectedSetIndex.value].id - const url = `/api/courses/system-question-sets/${originalId}` - const res = await $axios.put(url, payload) - message = `Set "${editedSet.name}" updated successfully` - - if (res.data) { - editedSet.id = res.data.id - editedSet.name = res.data.name - editedSet.questions = res.data.questions - } + let res + if (editedSet.id) { + res = await $axios.put(`/api/courses/system-question-set/${editedSet.id}`, { + name: editedSet.name, + questions: editedSet.questions + }) + showToast({ severity: 'success', summary: 'System questions updated successfully' }) } else { - const url = `/api/courses/system-question-sets/` - const res = await $axios.post(url, payload) - message = `Set "${editedSet.name}" created successfully` - - if (res.data) { - editedSet.id = res.data.id - editedSet.name = res.data.name - editedSet.questions = res.data.questions - } + res = await $axios.post(`/api/courses/system-question-set/`, { + name: editedSet.name, + questions: editedSet.questions + }) + showToast({ severity: 'success', summary: 'System questions created successfully' }) + } + if (res.data) { + editedSet.id = res.data.id + editedSet.name = res.data.name + editedSet.questions = res.data.questions } - - await loadQuestionSets() - showToast({ - severity: 'success', - summary: message - }) - router.push('/') } catch (err) { - // Handle server/backend errors const errorMsg = err.response?.data?.detail || 'An unexpected error occurred.' - showToast({ - severity: 'error', - summary: errorMsg - }) + showToast({ severity: 'error', summary: errorMsg }) } } async function deleteSet() { if (!editedSet.id) return - try { - const id = questionSets.value[selectedSetIndex.value!].id - await $axios.delete(`/api/courses/system-question-sets/${id}`) - showToast({ - severity: 'success', - summary: `Set "${editedSet.name}" deleted successfully` - }) - await loadQuestionSets() - router.push('/') + await $axios.delete(`/api/courses/system-question-set/${editedSet.id}`) + showToast({ severity: 'success', summary: 'System question set deleted successfully' }) + editedSet.id = null + editedSet.name = '' + editedSet.questions = [] } catch { - // @adeel: as we discussed, we want to get the error information from the backend - // and make sure the User knows what to do/ if its a server error - showToast({ - severity: 'error', - summary: 'Failed to delete question set' - }) + showToast({ severity: 'error', summary: 'Failed to delete system question set' }) } } -loadQuestionSets() +loadSystemQuestionSet() diff --git a/frontend/src/components/RegisterCourseSessionForm.vue b/frontend/src/components/RegisterCourseSessionForm.vue index 0ae8918..a6aaffb 100644 --- a/frontend/src/components/RegisterCourseSessionForm.vue +++ b/frontend/src/components/RegisterCourseSessionForm.vue @@ -12,6 +12,7 @@ const courses = ref<{ value: number; label: string }[]>([]) const locationsList = ref<{ value: number; label: string }[]>([]) const trainersList = ref<{ value: number; label: string }[]>([]) const systemQuestions = ref([]) +const courseQuestions = ref([]) interface Course { id: number @@ -128,6 +129,28 @@ onMounted(async () => { } }) +async function loadCourseQuestions(courseId: number) { + try { + const response = await $axios.get(`/api/courses/${courseId}/questions/`) + courseQuestions.value = response.data + } catch (error) { + courseQuestions.value = [] + showToast({ + severity: 'error', + summary: 'Failed to load course questions', + detail: error.response?.data?.message || error.message + }) + } +} + +async function onCourseChange(courseId: number) { + if (!courseId) { + courseQuestions.value = [] + return + } + await loadCourseQuestions(courseId) +} + function onQuestionSetsChange() { const combinedQuestions = [] @@ -180,7 +203,11 @@ function toISOStringWithTimezone(localDateTimeString: string) { } async function registerCourseSession() { - for (const [idx, q] of [...systemQuestions.value, ...questions.value].entries()) { + for (const [idx, q] of [ + ...systemQuestions.value, + ...questions.value, + ...courseQuestions.value + ].entries()) { if (!q.question.trim()) { showToast({ severity: 'error', @@ -221,7 +248,7 @@ async function registerCourseSession() { confirmation_deadline: toISOStringWithTimezone(confirmationDate.value), attendance_threshold: Number(attendanceThreshold.value) || 80, trainers: trainerIds.value, - questions: [...systemQuestions.value, ...questions.value] + questions: [...systemQuestions.value, ...questions.value, ...courseQuestions.value] } try { @@ -251,6 +278,7 @@ async function registerCourseSession() { name="course_id" :options="courses" validation="required" + @change="onCourseChange(selectedCourseId)" /> - +
    -

    Mandatory System Questions

    +

    Mandatory Questions

    - These questions cannot be edited and will always be asked during registration. + These questions cannot be edited and will be asked during registration in each session of + this course.

    Answer: Yes / No
    Answer: (free text)
    +
    +
    + {{ q.question }} +
    +
    Type: {{ q.type }}
    +
    +
    • {{ opt }}
    +
    +
    Answer: Yes / No
    +
    Answer: (free text)
    +
    -
    -- GitLab From bdfa378aeed549b3ff8f3c249c57b721d9ea6206 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Tue, 19 Aug 2025 08:35:15 +0000 Subject: [PATCH 26/32] backend: Threshold in text and file answers during user registration --- ...ype_alter_sessionquestion_type_and_more.py | 69 +++++++++++++++++++ backend/backend_trama/models/db_choices.py | 1 + backend/trama/api/courses.py | 23 +++++++ backend/trama/api/schemas.py | 2 +- backend/trama/settings.py | 4 ++ 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 backend/backend_trama/migrations/0005_alter_coursequestion_type_alter_sessionquestion_type_and_more.py diff --git a/backend/backend_trama/migrations/0005_alter_coursequestion_type_alter_sessionquestion_type_and_more.py b/backend/backend_trama/migrations/0005_alter_coursequestion_type_alter_sessionquestion_type_and_more.py new file mode 100644 index 0000000..1a46074 --- /dev/null +++ b/backend/backend_trama/migrations/0005_alter_coursequestion_type_alter_sessionquestion_type_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 5.2.4 on 2025-08-19 07:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("backend_trama", "0004_coursequestion"), + ] + + operations = [ + migrations.AlterField( + model_name="coursequestion", + name="type", + field=models.CharField( + choices=[ + ("single", "Single choice"), + ("multi", "Multiple choice"), + ("text", "Free text"), + ("yesno", "Yes/No"), + ("file", "File upload"), + ], + max_length=6, + ), + ), + migrations.AlterField( + model_name="sessionquestion", + name="type", + field=models.CharField( + choices=[ + ("single", "Single choice"), + ("multi", "Multiple choice"), + ("text", "Free text"), + ("yesno", "Yes/No"), + ("file", "File upload"), + ], + max_length=6, + ), + ), + migrations.AlterField( + model_name="systemquestion", + name="type", + field=models.CharField( + choices=[ + ("single", "Single choice"), + ("multi", "Multiple choice"), + ("text", "Free text"), + ("yesno", "Yes/No"), + ("file", "File upload"), + ], + max_length=6, + ), + ), + migrations.AlterField( + model_name="templatequestion", + name="type", + field=models.CharField( + choices=[ + ("single", "Single choice"), + ("multi", "Multiple choice"), + ("text", "Free text"), + ("yesno", "Yes/No"), + ("file", "File upload"), + ], + max_length=6, + ), + ), + ] diff --git a/backend/backend_trama/models/db_choices.py b/backend/backend_trama/models/db_choices.py index 28e2fc3..a681314 100644 --- a/backend/backend_trama/models/db_choices.py +++ b/backend/backend_trama/models/db_choices.py @@ -49,4 +49,5 @@ ANSWER_QUESTION_CHOICES = [ ("multi", "Multiple choice"), ("text", "Free text"), ("yesno", "Yes/No"), + ("file", "File upload"), ] diff --git a/backend/trama/api/courses.py b/backend/trama/api/courses.py index 4165e79..19f00e1 100644 --- a/backend/trama/api/courses.py +++ b/backend/trama/api/courses.py @@ -1,3 +1,4 @@ +import base64 from collections import defaultdict from backend_trama.models.db import ( @@ -12,6 +13,7 @@ from backend_trama.models.db import ( TemplateQuestion, TemplateQuestionSet, ) +from django.conf import settings from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -260,6 +262,27 @@ def register_for_session(request, session_id: int, payload: RegistrationCreateSc if Registration.objects.filter(course_session=session, participant=user).exists(): raise HttpError(409, "You are already registered for this session.") + for q_text, value in payload.answers.items(): + # Text character threshold + if isinstance(value, str): + if len(value) > settings.TEXT_CHAR_LIMIT: + raise HttpError( + 400, + f"Text answer for '{q_text}' too long " + f"(max {settings.TEXT_CHAR_LIMIT} characters)", + ) + + # File threshold + elif isinstance(value, dict) and "content" in value: + try: + raw = base64.b64decode(value["content"]) + except Exception: + raise HttpError(400, f"Invalid base64 content for file '{q_text}'") + + if len(raw) > settings.FILE_BYTE_LIMIT: + mb = settings.FILE_BYTE_LIMIT // (1024 * 1024) + raise HttpError(400, f"File '{q_text}' too large (max {mb} MB)") + Registration.objects.create( course_session=session, participant=user, diff --git a/backend/trama/api/schemas.py b/backend/trama/api/schemas.py index cf0cb16..70db658 100644 --- a/backend/trama/api/schemas.py +++ b/backend/trama/api/schemas.py @@ -31,7 +31,7 @@ class CourseSchema(ModelSchema): class QuestionSchema(Schema): question: str - type: Literal["single", "multi", "text", "yesno"] + type: Literal["single", "multi", "text", "yesno", "file"] options: list[str] = Field(default_factory=list) required: bool diff --git a/backend/trama/settings.py b/backend/trama/settings.py index 0a7b8a5..f3d6252 100644 --- a/backend/trama/settings.py +++ b/backend/trama/settings.py @@ -155,3 +155,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # TraMa specific settings CERTIFICATE_ATTENDANCE_THRESHOLD = 80 + +# Thresholds for Question Answers +TEXT_CHAR_LIMIT = 500 +FILE_BYTE_LIMIT = 15 * 1024 * 1023 # Change the first factor for number of MB -- GitLab From 43e91270295650f604786b8f96a3b3ec43a36c6d Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Tue, 19 Aug 2025 12:52:44 +0000 Subject: [PATCH 27/32] frontend: add file upload to the Question Editors/Creations --- frontend/src/components/QuestionSetEditor.vue | 3 ++- frontend/src/components/RegisterCourseForm.vue | 3 ++- frontend/src/components/RegisterCourseSessionForm.vue | 3 ++- frontend/src/components/SystemQuestionSetEditor.vue | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/QuestionSetEditor.vue b/frontend/src/components/QuestionSetEditor.vue index 3d5f729..a7ba2e0 100644 --- a/frontend/src/components/QuestionSetEditor.vue +++ b/frontend/src/components/QuestionSetEditor.vue @@ -13,7 +13,7 @@ const { showToast } = useToastHandler() interface Question { question: string - type: 'single' | 'multi' | 'text' | 'yesno' + type: 'single' | 'multi' | 'text' | 'yesno' | 'file' options: string[] required: boolean } @@ -228,6 +228,7 @@ loadQuestionSets() +
    diff --git a/frontend/src/components/RegisterCourseForm.vue b/frontend/src/components/RegisterCourseForm.vue index 2509d12..5042877 100644 --- a/frontend/src/components/RegisterCourseForm.vue +++ b/frontend/src/components/RegisterCourseForm.vue @@ -28,7 +28,7 @@ interface Unit { interface Question { question: string - type: 'single' | 'multi' | 'text' | 'yesno' + type: 'single' | 'multi' | 'text' | 'yesno' | 'file' options: string[] required: boolean isCustom?: boolean @@ -255,6 +255,7 @@ async function handleRegisterCourse() { +
    diff --git a/frontend/src/components/RegisterCourseSessionForm.vue b/frontend/src/components/RegisterCourseSessionForm.vue index a6aaffb..aeb01bd 100644 --- a/frontend/src/components/RegisterCourseSessionForm.vue +++ b/frontend/src/components/RegisterCourseSessionForm.vue @@ -63,7 +63,7 @@ const unitsList = ref<{ value: number; label: string }[]>([]) interface Question { question: string - type: 'single' | 'multi' | 'text' | 'yesno' + type: 'single' | 'multi' | 'text' | 'yesno' | 'file' options: string[] required: boolean } @@ -501,6 +501,7 @@ async function registerCourseSession() { +
    diff --git a/frontend/src/components/SystemQuestionSetEditor.vue b/frontend/src/components/SystemQuestionSetEditor.vue index 56094da..d57c406 100644 --- a/frontend/src/components/SystemQuestionSetEditor.vue +++ b/frontend/src/components/SystemQuestionSetEditor.vue @@ -7,7 +7,7 @@ const { showToast } = useToastHandler() interface Question { question: string - type: 'single' | 'multi' | 'text' | 'yesno' + type: 'single' | 'multi' | 'text' | 'yesno' | 'file' options: string[] required: boolean } @@ -172,6 +172,7 @@ loadSystemQuestionSet() +
    -- GitLab From 72608183f874ab72048f1043113a7bf1bf8e7591 Mon Sep 17 00:00:00 2001 From: Franziska Nicolaus Date: Tue, 19 Aug 2025 13:11:28 +0000 Subject: [PATCH 28/32] frontend: File upload and base64 conversion into JSON, Warning: no primevue file upload yet! --- .../src/components/CourseRegistrationForm.vue | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/frontend/src/components/CourseRegistrationForm.vue b/frontend/src/components/CourseRegistrationForm.vue index da631a1..c36ad75 100644 --- a/frontend/src/components/CourseRegistrationForm.vue +++ b/frontend/src/components/CourseRegistrationForm.vue @@ -4,6 +4,9 @@ import { useRouter } from 'vue-router' import $axios from '@/interceptors/axios' import { useToastHandler } from '@/composables/useToastHandler' import { getSessionQuestions, getSessionDetails } from '@/api/coursesApi' +// TODO +// import FileUpload from 'primevue/fileupload' +// import { useBase64 } from '@vueuse/core' const props = defineProps({ sessionId: { @@ -52,6 +55,30 @@ async function loadSessionDetails() { } } +async function handleFileUpload(file: File, qId: number) { + // @ adeel: is it possible to somehow import the settings from the backend? In backend/trama/settings.py the limits are already set, which should be usable globally + const FILE_BYTE_LIMIT = 15 * 1024 * 1024 // change first factor for number of MB + + if (file.size > FILE_BYTE_LIMIT) { + const fileMBLimit = FILE_BYTE_LIMIT / (1024 * 1024) + + showToast({ + severity: 'warn', + summary: 'File too large', + detail: `The selected file exceeds the ${fileMBLimit}MB limit.` + }) + return + } + // @ adeel: this is working, better useBase64 from @vueuse/core? + const reader = new FileReader() // in-built file reader + // onload gets triggert after the file is fully read with readAsDataURL, asynchronous process + reader.onload = () => { + const base64 = (reader.result as string).split(',')[1] // cuts the part in URI that is the actual base64 content + answers.value[qId] = { name: file.name, content: base64 } + } + reader.readAsDataURL(file) // Asynchronous reading process, produces data URI like 'data:image/png;base64,iVBORw0KGgoAAAANS', after that, onload gets triggert +} + async function submitForm() { for (const q of questions.value) { const ans = answers.value[q.id] @@ -171,6 +198,22 @@ onMounted(() => {
    + +
    + +