1
Fork 0
mirror of https://github.com/Steffo99/sophon.git synced 2024-12-22 06:44:21 +00:00

📔 Complete?

This commit is contained in:
Steffo 2021-11-05 17:55:15 +01:00
parent ee19cfcd63
commit 2856fd0d57
Signed by: steffo
GPG key ID: 6965406171929D01
8 changed files with 465 additions and 38 deletions

View file

@ -0,0 +1,17 @@
Librerie e tecnologie utilizzate
--------------------------------
.. note::
Sono elencate solo le principali librerie utilizzate; dipendenze e librerie minori non sono specificate, ma sono visibili all'interno del file ``yarn.lock``.
- I linguaggi di programmazione `JavaScript <https://developer.mozilla.org/en-US/docs/Web/JavaScript/About_JavaScript>`_ e `TypeScript <https://www.typescriptlang.org/>`_
- Il gestore di dipendenze `Yarn <https://yarnpkg.com/>`_
- La libreria grafica `Bluelib <https://github.com/Steffo99/bluelib>`_ (sviluppata come progetto personale nell'estate 2021)
- Il framework per interfacce grafiche `React <https://reactjs.org>`_
- Il router `Reach Router <https://reach.tech/router/>`_
- L'integrazione con React di Bluelib `bluelib-react <https://github.com/Steffo99/bluelib-react>`_ (sviluppata durante il tirocinio)
- Il componente React `react-markdown <https://github.com/remarkjs/react-markdown>`_
- Il framework per testing `Jest <https://jestjs.io/>`_
- Un fork personalizzato del client XHR `axios <https://github.com/axios/axios>`_
- Il webserver statico `serve <https://www.npmjs.com/package/serve>`_

View file

@ -0,0 +1,23 @@
Struttura delle directory
-------------------------
.. default-domain:: js
Le directory di :mod:`@steffo45/sophon-frontend` sono strutturate nella seguente maniera:
src/components
Contiene i componenti React sia con le classi sia funzionali.
src/contexts
Contiene i contesti React creati con :func:`React.createContext`.
src/hooks
Contiene gli hook React personalizzati utilizzati nei componenti funzionali.
src/types
Contiene estensioni ai tipi base TypeScript, come ad esempio i tipi restituiti dalla web API del :ref:`modulo backend`.
src/utils
Contiene varie funzioni di utility.
public
Contiene i file statici da servire assieme all'app.

View file

@ -0,0 +1,113 @@
Comunicazione con il server
---------------------------
.. default-domain:: js
Axios
^^^^^
Per effettuare richieste all'API web, si è deciso di utilizzare la libreria :mod:`axios`, in quanto permette di creare dei "client" personalizzabili con varie proprietà.
In particolare, si è scelto di forkarla, integrando anticipatamente una proposta di funzionalità che permette alle richieste di essere interrotte attraverso degli :class:`AbortController`.
Client personalizzati
^^^^^^^^^^^^^^^^^^^^^
Per permettere all'utente di selezionare l'istanza da utilizzare e di comunicare con l'API con le proprie credenziali, si è scelto di creare client personalizzati partendo da due contesti.
All'interno di un contesto in cui è stata selezionata un'istanza (:data:`InstanceContext`), viene creato un client dal seguente hook:
.. function:: useInstanceAxios(config = {})
Questo hook specifica il ``baseURL`` del client Axios, impostandolo all'URL dell'istanza selezionata.
All'interno di un contesto in cui è stato effettuato l'accesso come utente (:data:`AuthorizationContext`), viene creato invece un client dal seguente hook:
.. function:: useAuthorizedAxios(config = {})
Questo hook specifica il valore dell'header ``Authorization`` da inviare in tutte le richieste effettuate a :samp:`Bearer {TOKEN}`, utilizzando il token ottenuto al momento dell'accesso.
Utilizzo di viewset
^^^^^^^^^^^^^^^^^^^
Viene implementato un hook che si integra con i viewset di Django, fornendo un API semplificato per effettuare azioni su di essi.
.. function:: useViewSet(baseRoute)
Questo hook implementa tutte le azioni :py:mod:`rest_framework` di un viewset in lettura e scrittura.
Richiede di essere chiamato all'interno di un :data:`AuthorizationContext`.
.. function:: async list(config = {})
.. function:: async retrieve(pk, config = {})
.. function:: async create(config)
.. function:: async update(pk, config)
.. function:: async destroy(pk, config)
Viene inoltre fornito supporto per le azioni personalizzate.
.. function:: async command(config)
Permette azioni personalizzate su tutto il viewset.
.. function:: async action(config)
Permette azioni personalizzate su uno specifico oggetto del viewset.
Emulazione di viewset
^^^^^^^^^^^^^^^^^^^^^
Viene creato un hook che tiene traccia degli oggetti restituiti da un determinato viewset, ed emula i risultati delle azioni effettuate, minimizzando i rerender e ottenendo una ottima user experience.
.. function:: useManagedViewSet(baseRoute, pkKey, refreshOnMount)
.. attribute:: viewset
Il viewset restituito da :func:`useViewSet`, utilizzato come interfaccia di basso livello per effettuare azioni.
.. attribute:: state
Lo stato del viewset, che tiene traccia degli oggetti e delle azioni in corso su di essi.
Gli oggetti all'interno di esso sono istanze di :class:`ManagedResource`, create usando wrapper di :func:`.update`, :func:`.destroy` e :func:`.action`, che permettono di modificare direttamente l'oggetto senza preoccuparsi dell'indice a cui si trova nell'array.
.. attribute:: dispatch
Riduttore che permette di alterare lo :attr:`.state`.
.. function:: async refresh()
Ricarica gli oggetti del viewset.
Viene chiamata automaticamente al primo render se ``refreshOnMount`` è :data:`True`.
.. function:: async create(data)
Crea un nuovo oggetto nel viewset con i dati specificati come argomento, e lo aggiunge allo stato se la richiesta va a buon fine.
.. function:: async command(method, cmd, data)
Esegue l'azione personalizzata ``cmd`` su tutto il viewset, utilizzando il metodo ``method`` e con i dati specificati in ``data``.
Se la richiesta va a buon fine, il valore restituito dal backend sostituisce nello stato le risorse dell'intero viewset.
.. function:: async update(index, data)
Modifica l'oggetto alla posizione ``index`` dell'array :attr:`.state` con i dati specificati in ``data``.
Se la richiesta va a buon fine, la modifica viene anche applicata all'interno di :attr:`.state`
.. function:: async destroy(index)
Elimina l'oggetto alla posizione ``index`` dell'array :attr:`.state`.
Se la richiesta va a buon fine, l'oggetto viene eliminato anche da :attr:`.state`.
.. function:: async action(index, method, act, data)
Esegue l'azione personalizzata ``act`` sull'oggetto alla posizione ``index`` dell'array :attr:`.state`, utilizzando il metodo ``method`` e con i dati specificati in ``data``.
Se la richiesta va a buon fine, il valore restituito dal backend sostituisce l'oggetto utilizzato in :attr:`.state`.

View file

@ -0,0 +1,161 @@
Contesti innestati
------------------
Per minimizzare i rerender, l'applicazione è organizzata a "contesti innestati".
I contesti
^^^^^^^^^^
Viene definito un contesto per ogni tipo di risorsa selezionabile nell'interfaccia.
Essi sono, in ordine dal più esterno al più interno:
#. :data:`InstanceContext` (:ref:`Istanza`)
#. :data:`AuthorizationContext` (:ref:`Utente`)
#. :data:`GroupContext` (:ref:`Gruppo di ricerca`)
#. :data:`ProjectContext` (:ref:`Progetto di ricerca`)
#. :data:`NotebookContext` (:ref:`Notebook`)
Contenuto dei contesti
""""""""""""""""""""""
Questi contesti possono avere tre tipi di valori: :data:`undefined` se ci si trova al di fuori del contesto, :data:`null` se non è stato selezionato alcun oggetto oppure **l'oggetto selezionato** se esso esiste.
URL contestuale
^^^^^^^^^^^^^^^
Si è definita la seguente struttura per gli URL del frontend di Sophon, in modo che essi identificassero universalmente una risorsa e che essi fossero human-readable.
.. code-block:: text
/i/{ISTANZA}
/l/logged-in
/g/{GROUP_SLUG}
/p/{PROJECT_SLUG}
/n/{NOTEBOOK_SLUG}/
Ad esempio, l'URL per il notebook ``my-first-notebook`` dell'istanza demo di Sophon sarebbe:
.. code-block:: text
/i/https:api.prod.sophon.steffo.eu:
/l/logged-in
/g/my-first-group
/p/my-first-project
/n/my-first-notebook/
Parsing degli URL contestuali
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Viene definita una funzione in grado di comprendere gli URL contestuali:
.. function:: parsePath(path)
:param path: Il "path" da leggere.
:returns:
Un oggetto con le seguenti chiavi, dette "segmenti di percorso", le quali possono essere :data:`undefined` per indicare che non è stato selezionato un oggetto di quel tipo:
- ``instance``: l'URL dell'istanza da utilizzare, con caratteri speciali sostituiti da ``:``
- ``loggedIn``: :class:`Boolean`, se :data:`True` l'utente ha effettuato il login (come :ref:`Ospite` o :ref:`Utente`)
- ``researchGroup``: lo slug del :ref:`gruppo di ricerca` selezionato
- ``researchProject``: lo slug del :ref:`progetto di ricerca` selezionato
- ``notebook``: lo slug del :ref:`notebook` selezionato
Ad esempio, l'URL precedente restituirebbe il seguente oggetto se processato:
.. code-block:: js
{
"instance": "https:api.prod.sophon.steffo.eu:",
"loggedIn": True,
"researchGroup": "my-first-group",
"researchProject": "my-first-project",
"notebook": "my-first-notebook"
}
Routing basato sui contesti
^^^^^^^^^^^^^^^^^^^^^^^^^^^
I valori dei contesti vengono utilizzati per selezionare i componenti da mostrare all'utente nell'interfaccia grafica attraverso i seguenti componenti:
.. function:: ResourceRouter({selection, unselectedRoute, selectedRoute})
Componente che sceglie se renderizzare ``unselectedRoute`` o ``selectedRoute`` in base alla *nullità* o *non-nullità* di ``selection``.
.. function:: ViewSetRouter({viewSet, unselectedRoute, selectedRoute, pathSegment, pkKey})
Componente basato su :func:`ResourceRouter` che seleziona automaticamente l'elemento del viewset avente il valore del segmento di percorso ``pathSegment`` alla chiave ``pkKey``.
Esempio di utilizzo di ViewSetRouter
""""""""""""""""""""""""""""""""""""
.. function:: GroupRouter({...props})
Implementato come:
.. code-block:: tsx
<ViewSetRouter
{...props}
viewSet={useManagedViewSet<SophonResearchGroup>("/api/core/groups/", "slug")}
pathSegment={"researchGroup"}
pkKey={"slug"}
/>
Albero completo dei contesti
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
L'insieme di tutti i contesti è definito come componente :func:`App` nel modulo "principale" ``App.tsx``.
Se ne riassume la struttura in pseudocodice:
.. code-block:: html
<InstanceContext>
<InstanceRouter>
unselected:
<InstanceSelect>
selected:
<AuthorizationContext>
<AuthorizationRouter>
unselected:
<UserLogin>
selected:
<GroupContext>
<GroupRouter>
unselected:
<GroupSelect>
selected:
<ProjectContext>
<ProjectRouter>
unselected:
<ProjectSelect>
selected:
<NotebookContext>
<NotebookRouter>
unselected:
<NotebookSelect>
selected:
<NotebookDetails>
Altri contesti
^^^^^^^^^^^^^^
Tema
""""
Il tema dell'istanza è implementato come uno speciale contesto globale :data:`ThemeContext` che riceve i dettagli dell'istanza a cui si è collegati dall':data:`InstanceContext`.
Cache
"""""
Viene salvato l'elenco di tutti i membri dell':ref:`istanza` in uno speciale contesto :data:`CacheContext` in modo da poter risolvere gli id degli utenti al loro username senza dover effettuare ulteriori richieste.

View file

@ -8,28 +8,12 @@ Si è cercato di renderla più user-friendly possibile, cercando di comunicare p
È collocato all'interno del repository in ``/frontend``. È collocato all'interno del repository in ``/frontend``.
Il modulo è formato dal package JavaScript :mod:`@steffo45/sophon-frontend`, che contiene tutti i componenti React che assemblati insieme formano l'intera interfaccia web.
Librerie e tecnologie utilizzate .. toctree::
-------------------------------- :maxdepth: 1
.. note::
Sono elencate solo le principali librerie utilizzate; dipendenze e librerie minori non sono specificate, ma sono visibili all'interno del file ``yarn.lock``.
- I linguaggi di programmazione `JavaScript <https://developer.mozilla.org/en-US/docs/Web/JavaScript/About_JavaScript>`_ e `TypeScript <https://www.typescriptlang.org/>`_
- Il gestore di dipendenze `Yarn <https://yarnpkg.com/>`_
- La libreria grafica `Bluelib <https://github.com/Steffo99/bluelib>`_ (sviluppata come progetto personale nell'estate 2021)
- Il framework per interfacce grafiche `React <https://reactjs.org>`_
- Il router `Reach Router <https://reach.tech/router/>`_
- L'integrazione con React di Bluelib `bluelib-react <https://github.com/Steffo99/bluelib-react>`_ (sviluppata durante il tirocinio)
- Il componente React `react-markdown <https://github.com/remarkjs/react-markdown>`_
- Il framework per testing `Jest <https://jestjs.io/>`_
- Un fork personalizzato del client XHR `axios <https://github.com/axios/axios>`_
- Il webserver statico `serve <https://www.npmjs.com/package/serve>`_
Struttura del modulo
--------------------
Il modulo consiste nel package JavaScript :mod:`@steffo45/sophon-frontend`, che contiene tutti i componenti che assemblati insieme formano l'intera interfaccia web.
1_techstack
2_tree
3_resources
4_contexts

View file

@ -1,3 +1,132 @@
Test effettuati Test effettuati
=============== ===============
Per motivi di tempo necessario, sono state selezionate solo alcune parti di codice su cui effettuare test.
Tutti i viewset di :py:mod:`sophon.core`
----------------------------------------
.. default-domain:: py
.. default-role:: py:obj
.. module:: sophon.core.tests
Test case generici
^^^^^^^^^^^^^^^^^^
Vengono definiti alcuni test case generici per facilitare le interazioni tra ``APITestCase`` e viewset.
.. note::
I nomi delle funzioni usano nomi con capitalizzazione inconsistente in quanto lo stesso modulo `unittest` non rispetta lo stile suggerito in :pep:`8`.
.. class:: BetterAPITestCase(APITestCase)
.. method:: as_user(self, username: str, password: str = None) -> typing.ContextManager[None]
Context manager che permette di effettuare richieste all'API come uno specifico utente, effettuando il logout quando sono state effettuate le richieste necessarie.
.. method:: assertData(self, data: ReturnDict, expected: dict)
Asserzione che permette di verificare che l'oggetto restituito da una richiesta all'API contenga almeno le chiavi e i valori contenuti nel dizionario ``expected``.
.. class:: ReadSophonTestCase(BetterAPITestCase, metaclass=abc.ABCMeta)
Classe **astratta** che implementa metodi per testare rapidamente le azioni di un `.views.ReadSophonViewSet`.
.. classmethod:: get_basename(cls) -> str
Metodo **astratto** che deve restituire il basename del viewset da testare.
.. classmethod:: get_url(cls, kind: str, *args, **kwargs) -> str
Metodo utilizzato dal test case per trovare gli URL ai quali possono essere effettuate le varie azioni.
I seguenti metodi permettono di effettuare azioni sul viewset:
.. method:: list(self) -> rest_framework.response.Response
.. method:: retrieve(self, pk) -> rest_framework.response.Response
.. method:: custom_list(self, method: str, action: str, data: dict = None) -> rest_framework.response.Response
.. method:: custom_detail(self, method: str, action: str, pk, data: dict = None) -> rest_framework.response.Response
I seguenti metodi asseriscono che una determinata azione con determinati parametri risponderà con il codice di stato ``code``, e restituiscono i dati contenuti nella risposta se l'azione è riuscita (``200 <= code < 300``)
.. method:: assertActionList(self, code: int = 200) -> typing.Optional[ReturnDict]
.. method:: assertActionRetrieve(self, pk, code: int = 200) -> typing.Optional[ReturnDict]
.. method:: assertActionCustomList(self, method: str, action: str, data: dict = None, code: int = 200) -> typing.Optional[ReturnDict]
.. method:: assertActionCustomDetail(self, method: str, action: str, pk, data: dict = None, code: int = 200) -> typing.Optional[ReturnDict]
.. class:: WriteSophonTestCase(ReadSophonTestCase, metaclass=abc.ABCMeta)
Classe **astratta** che estende `.ReadSophonTestCase` con le azioni di un `.views.WriteSophonViewSet`.
.. method:: create(self, data) -> rest_framework.response.Response
.. method:: update(self, pk, data) -> rest_framework.response.Response
.. method:: destroy(self, pk) -> rest_framework.response.Response
.. method:: assertActionCreate(self, data, code: int = 201) -> typing.Optional[ReturnDict]
.. method:: assertActionUpdate(self, pk, data, code: int = 200) -> typing.Optional[ReturnDict]
.. method:: assertActionDestroy(self, pk, code: int = 200) -> typing.Optional[ReturnDict]
Test case concreti
^^^^^^^^^^^^^^^^^^
Vengono testate tutte le view dell'app tramite `.BetterAPITestCase` e tutti i viewset dell'app tramite `.ReadSophonTestCase` e `WriteSophonTestCase`.
.. class:: UsersByIdTestCase(ReadSophonTestCase)
.. class:: UsersByUsernameTestCase(ReadSophonTestCase)
.. class:: ResearchGroupTestCase(WriteSophonTestCase)
.. class:: SophonInstanceDetailsTestCase(BetterAPITestCase)
Alcune interazioni di `sophon.notebooks`
----------------------------------------
.. default-domain:: py
.. default-role:: py:obj
.. module:: sophon.notebooks.tests
Vengono definiti alcuni test case per alcune interazioni dell'app `sophon.notebooks`.
.. class:: JupyterTestCase(TestCase)
Test case che testa la generazione dei token per Jupyter.
.. class:: ApacheTestCase(TestCase)
Test case che testa la conversione in `bytes` per la rubrica `dbm` del :ref:`modulo proxy`.
Alcune interazioni complicate del frontend
------------------------------------------
.. default-domain:: js
.. default-role:: js:class
Vengono infine definiti test case per alcune interazioni ritenute particolarmente complesse del frontend.
Encoding dell'URL dell'istanza nell'URL della pagina
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- encodes pathless URL
- encodes URL with port number
- encodes URL with simple path
- encodes URL with colon in path
- does not encode URL with ``%3A`` in path
- decodes pathless URL
- decodes URL with port number
- decodes URL with simple path
- decodes URL with colon in path
Parsing dei segmenti del path
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- parses empty path
- parses instance path
- parses username path
- parses userid path
- parses research group path
- parses research project path
- parses research project path

View file

@ -1,58 +1,58 @@
import { parsePath } from "./ParsePath" import { parsePath } from "./ParsePath"
test("splits empty path", () => { test("parses empty path", () => {
expect( expect(
parsePath("/"), parsePath("/"),
).toMatchObject( ).toMatchObject(
{} {},
) )
}) })
test("splits instance path", () => { test("parses instance path", () => {
expect( expect(
parsePath("/i/https:api:sophon:steffo:eu:"), parsePath("/i/https:api:sophon:steffo:eu:"),
).toMatchObject( ).toMatchObject(
{ {
instance: "https:api:sophon:steffo:eu:" instance: "https:api:sophon:steffo:eu:",
} },
) )
}) })
test("splits username path", () => { test("parses username path", () => {
expect( expect(
parsePath("/i/https:api:sophon:steffo:eu:/u/steffo"), parsePath("/i/https:api:sophon:steffo:eu:/u/steffo"),
).toMatchObject( ).toMatchObject(
{ {
instance: "https:api:sophon:steffo:eu:", instance: "https:api:sophon:steffo:eu:",
userName: "steffo", userName: "steffo",
} },
) )
}) })
test("splits userid path", () => { test("parses userid path", () => {
expect( expect(
parsePath("/i/https:api:sophon:steffo:eu:/u/1"), parsePath("/i/https:api:sophon:steffo:eu:/u/1"),
).toMatchObject( ).toMatchObject(
{ {
instance: "https:api:sophon:steffo:eu:", instance: "https:api:sophon:steffo:eu:",
userId: "1", userId: "1",
} },
) )
}) })
test("splits research group path", () => { test("parses research group path", () => {
expect( expect(
parsePath("/i/https:api:sophon:steffo:eu:/g/testers"), parsePath("/i/https:api:sophon:steffo:eu:/g/testers"),
).toMatchObject( ).toMatchObject(
{ {
instance: "https:api:sophon:steffo:eu:", instance: "https:api:sophon:steffo:eu:",
researchGroup: "testers", researchGroup: "testers",
} },
) )
}) })
test("splits research project path", () => { test("parses research project path", () => {
expect( expect(
parsePath("/i/https:api:sophon:steffo:eu:/g/testers/p/test"), parsePath("/i/https:api:sophon:steffo:eu:/g/testers/p/test"),
).toMatchObject( ).toMatchObject(
@ -60,11 +60,11 @@ test("splits research project path", () => {
instance: "https:api:sophon:steffo:eu:", instance: "https:api:sophon:steffo:eu:",
researchGroup: "testers", researchGroup: "testers",
researchProject: "test", researchProject: "test",
} },
) )
}) })
test("splits research project path", () => { test("parses research project path", () => {
expect( expect(
parsePath("/i/https:api:sophon:steffo:eu:/g/testers/p/test/n/testerino"), parsePath("/i/https:api:sophon:steffo:eu:/g/testers/p/test/n/testerino"),
).toMatchObject( ).toMatchObject(
@ -73,6 +73,6 @@ test("splits research project path", () => {
researchGroup: "testers", researchGroup: "testers",
researchProject: "test", researchProject: "test",
notebook: "testerino", notebook: "testerino",
} },
) )
}) })