Johdatus TIMin kehitykseen
Dokumentissa käydään läpi TIMin kehitykseen liittyvät olennaisimmat perusasiat.
1. TIM-järjestelmän kokonaisrakenne
TIM-järjestelmän kokonaisrakenne on esitetty alla olevassa kaaviossa. Se koostuu Docker-konteista, jotka kuvassa näkyvät tummennettuina laatikkoina.
Caddy-palvelin vastaanottaa selaimelta tulevan pyynnön ja yleensä välittää sen TIM-kontille. Jotkut staattisten tiedostojen pyynnöt (esim. /static/*
) Caddy käsittelee suoraan itse, ja osa pyynnöistä menee suoraan pluginkonteille kiertämättä TIM-kontin kautta. Nämä on määritetty tiedostossa Caddyfile.
Jos ollaan näyttämässä dokumenttia ja jos sen HTML ei ole vielä välimuistissa, niin sitä pyydetään Dumbolta. Lisäksi, jos dokumentissa on liitännäislohkoja, niin tehdään pyyntöjä tarvittaville pluginkonteille, jotka palauttavat pluginin HTML-muodossa sekä mahdollisten sen tarvitsemien JS- ja CSS-tiedostojen osoitteet.
Caddyfile-linkki on vanhentunut.
DZ: Korjattu
—Onko tuo Dumbo tosiaan näin välttämätön? Sain TIMin lokaalisti ihan nätisti pystyyn edes kloonaamatta Dumbo-repoa omalle koneelle.
DZ: Dumbo on kyllä välttämätön dokumenttien näyttämisen kannalta. Nykyään Dumbon lähdekoodia ei tarvitse erikseen ladata, vaan Dumbo ladataan automaattisesti Docker-pohjakuvana. Jos jossain on mainintaa Dumbon lähdekoodin lataamisesta, niin se pitää korjata :)
—1.1 TIM
TIMin ydin (kaaviossa TIM) on toteutettu Pythonilla (versio 3.10) käyttäen Flask-sovelluskehystä.
1.2 Dumbo
Dumbo on Haskell-kielellä toteutettu komponentti, jonka tehtävänä on muuntaa dokumentin merkintäkieltä HTML-muotoon.
2. Dokumentit
- Dokumentti = lista lohkoja.
- Jokaisella lohkolla on:
- id: yksikäsitteinen tunniste dokumentin sisällä
- md: markdown-teksti
- 0 tai useampia käyttäjän määrittämiä attribuutteja ja luokkia
- t: sisällöstä ja attribuuteista laskettu tiiviste
- HTML-välimuisti
- Tehtävälohkoilla oltava attribuutit
taskId
japlugin
- Kun lohkolista muunnetaan tekstiksi (esim. Manage-näkymä), lohkot erotetaan toisistaan seuraavilla säännöillä:
- Jos lohko alkaa otsikolla (
# ...
), lohkoon ei tarvita erillistä erotinmerkkiä. Lohkon attribuutit lisätään otsikon perään. - Jos lohko on koodilohko (tai plugin) eli se alkaa ``` + rivinvaihto, erotinmerkkiä ei tällöinkään tarvita ja toimitaan vastaavasti kuten otsikon tapauksessa.
- Muussa tapauksessa lohkon alkuun lisätään
#-
-rivi, jonka perään attribuutit lisätään kuten otsikon tapauksessa. - Edellisten sääntöjen avulla lohkolistateksti voidaan jäsentää takaisin yksittäisiksi lohkoiksi.
- Jos lohko alkaa otsikolla (
2.1 Merkintäkieli
Dokumenttien merkintäkielenä käytetään Pandoc-markdownia sen oletuslaajennuksilla. Lisäksi merkintäkieleen on tehty Pythonilla laajennus, joka mahdollistaa luentokalvojen tauottamisen. Pandocissakin on vastaavanlainen ominaisuus, mutta se ei ole yhtä joustava.
2.1.1 Python-laajennus
Python-laajennus suoritetaan Dumbon prosessoinnin jälkeen, eli laajennus käsittelee HTML-kieltä. Lohko <§
... §>
määrittää tauon, ja tauon sisällä voi tauottaa esimerkiksi yksittäisiä sanoja laittamalla ne §§
...§§
sisään.
Esimerkiksi dokumentissa:
# Otsikko1
#-
Tekstiä
#-
yksi
<§
kaksi
* §§kolme§§
* §§nelj䧧
§>
# Otsikko2
on 2 kalvoa, joista ensimmäisessä näkyy aluksi vain
Otsikko1
,Tekstiä
jayksi
.
Tämän jälkeen tekstit
kaksi
kolme
neljä
näytetään kukin omalla askeleellaan. Seuraavalla askeleella kalvo vaihtuu eli näkyviin tulee Otsikko2
.
2.2 Esimerkki lohkosta
Lohkolla
#- {id="AiKFDMMCQ9gS" .red .huomautus a="b" c="d"}
Terve!
on seuraavat ominaisuudet:
- id on
AiKFDMMCQ9gS
- md on
Terve!
- luokkia ovat
red
jahuomautus
- attribuutteja ovat
a
jac
, joiden arvot ovat vastaavastib
jad
. - t on
MHg4ZjM2YzRk
(tämä on aina piilossa käyttäjältä)
Lohkon talletusmuoto on seuraavanlainen JSON:
Yllä olevassa JSONissa h
sisältää lohkon HTML-välimuistin. Jono LTB4NTEzYzFkZTk=
on dokumentin asetuksista sekä dokumentin sisäisistä laskurimakroista riippuva tiiviste. Sen arvo muuttuu aina, kun dokumentin asetuksia muutetaan tai kun ennen lohkoa lisätään otsikko, joka muuttaa/muuttaisi otsikkonumerointia. Tällöin HTML osataan laskea uudelleen, jolloin vanha välimuistiavain pyyhitään ja uusi laitetaan tilalle.
Yksittäistä lohkoa muokatessa sen id:tä ei näytetä.
2.3 Dokumentin esittäminen
Dokumentti esitetään, kun kutsutaan jotakin reiteistä:
/view/polku
/teacher/polku
/answers/polku
/lecture/polku
/velp/polku
Esittämisfunktio on tiedostossa item/routes.py. Se etenee likimain seuraavasti:
- katsotaan, löytyykö polusta dokumenttia
- jos ei, katsotaan, löytyykö kansio, jos ei, näytetään "not found"
- tarkistetaan, onko käyttäjällä dokumenttiin oikeus (ja millainen)
- jos dokumentti on käyttäjällä välimuistissa, ladataan se sieltä ja hypätään suoraan muodostamaan lopullinen HTML
Jos dokumentti ei ollut välimuistissa, jatketaan seuraavasti:
- ladataan dokumentin lohkot tiedostojärjestelmästä
- lasketaan lohkoviittaukset auki, jos sellaisia on
- etsitään dokumentista pluginit
- haetaan käyttäjän viimeisimmät vastaukset/kenttien arvot plugineihin
- kutsutaan kunkin plugintyypin kohdalla pluginin multihtml-reittiä ja asetetaan saadut HTML:t paikoilleen dokumenttiin (funktio pluginify)
- haetaan dokumenttiin liittyvät kommentit ja laitetaan ne paikoilleen
- haetaan lukumerkintätiedot ja laitetaan ne paikoilleen
- muodostetaan lopullinen HTML ja lähetetään se selaimelle
Liittyykö tuo linkitetty funktio varmasti dokumentin esittämiseen? Omien tutkimusten mukaan kutsupino funktiosta, johon Flask reitittää HTTP-kutsun, kappalesisällön hakuun tiedostosta etenee polkua: routes.py/view_document -> routes.py/view -> routes.py/render_doc_view -> routes.py/get_document -> routes.py/get_partial_document -> Document.get_paragraps -> Document.ensure_pars_loaded -> Document.load_pars -> Document.load_pars -> Document.__iter__ -> DocParagraphIter.__next__ -> DocParagraph.get Tuohon gen_cache-funktioon ei grep-ohjelman perusteella viittata missään muualla repossa.
DZ: Kiitos tiedosta, olisikohan tuo typo tai joku vanha linkki sitten. Tässä tapauksessa sen pitäisi viitata view
-funktioon samassa tiedostossa. Korjasin nyt linkin.
3. Palvelinpuolen kehitys
3.1 Reitit
Palvelimen reitit (eli kutsuttavissa olevat URL-osoitteet) on jaoteltu Flaskin blueprintteihin. Yksi blueprint kokoaa yhteen samaan kokonaisuuteen liittyvät reitit.
Esimerkkejä nykyisistä blueprinteistä:
- answer.py: Vastauksiin liittyvä toiminnallisuus
- bookmark/routes.py: Kirjanmerkkeihin liittyvä toiminnallisuus
- login.py: Kirjautumiseen liittyvä toiminnallisuus
Esimerkki flaskin reittien käyttämisestä:
3.1.1 Poikkeuksien käsittely
Älä käytä Flaskin abort
-funktiota, vaan heitä poikkeus, jos reitin suorittaminen pitää pysäyttää (esim. oikeuksia puuttuu, jotakin objektia ei ole olemassa, tms.). Syitä:
- Erilaiset koodianalysointityökalut (kuten Mypy) ymmärtävät, että aliohjelman suoritus loppuu siihen paikkaan. Jos poikkeuksia ei käytä, analysoija saattaa virheellisesti varoittaa alustamattomista muuttujista.
- HTTP-statuskoodia ei tarvitse toistaa koodissa joka paikassa.
Yleisimpiä poikkeusluokkia:
AccessDenied
kun oikeuksia puuttuuNotExist
kun jotakin objektia ei löydyRouteException
muille syille
3.1.2 Oikeudet
Kuka tahansa käyttäjä ei tietenkään saa kutsua mitä tahansa reittiä. Tätä varten on accesshelper
, joka tarjoaa funktiota oikeuksien tarkistukseen. Olennaisimpia funktioita ovat seuraavat:
verify_view_access
: Jos käyttäjällä ei ole view-oikeutta objektiin, lopetetaan pyynnön käsittely ja palautetaan virheilmoitus puuttuvasta oikeudesta.verify_edit_access
: Kutenverify_view_access
, mutta edit-oikeudelle.verify_manage_access
: Kutenverify_view_access
, mutta manage-oikeudelle.verify_seeanswers_access
: Kutenverify_view_access
, mutta see answers -oikeudelle.verify_teacher_access
: Kutenverify_view_access
, mutta teacher-oikeudelle.verify_ownership
: Kutenverify_view_access
, mutta owner-oikeudelle.verify_admin
: Jos käyttäjä ei ole ylläpitäjä, lopetetaan pyynnön käsittely ja palautetaan virheilmoitus puuttuvasta oikeudesta.
3.2 Tietokanta
Tietokannan rakenne on määritelty SQLAlchemyn malliluokkien kautta.
Nykyiset malliluokat sijaitsevat eri paikoissa riippuen siitä, mihin toimintokokonaisuuteen se liittyy. Tiedostossa timApp/tim_app.py luetellaan kaikki malliluokat.
Dokumenttien sisältö ja ladatut tiedostot tallennetaan tiedostojärjestelmään. Muu tieto tallennetaan tietokantaan.
Olennaisimmat taulut (malliluokat) tietokannassa ovat seuraavat:
- UserAccount: Käyttäjät
- UserGroup: Käyttäjäryhmät
- Answer: Tehtävien vastaukset
taskId
tallennetaan kantaan muodossadocId.taskId
, missädocId
on dokumentin tunnistenumero jataskId
tehtävälohkoon kirjoitettu attribuutti.
- Block: Taulu, johon tallennetaan metatietoa objekteista, jotka tarvitsevat oikeuksien hallintaa (toistaiseksi dokumentit, kansiot, ladatut kuvat ja ladatut tiedostot)
- DocEntry: Dokumentit ja niiden aliakset
- Folder: Kansiot
Tietokantaoperaatiot määritellään SQLAlchemyn operaatioiden avulla, eli TIMissä ei käytetä missään "raakaa SQL:ää".
3.2.1 Malliluokan (taulun) luonti tai muuttaminen
Uuden taulun luominen:
- Luo malliluokka perien se
db.Model
-luokasta. - Rekisteröi se moduuliin
tim_app.py
. - Aja komentorivillä
./tim run flask db migrate -m "Add table for xxx"
(parametrissa-m
anna selitys taulun tarkoituksesta). - Skripti ilmoittaa tiedoston, joka luotiin hakemistoon
timApp/migrations/versions
. Avaa se ja tarkista, että siinä ei ole virheitä. - Aja
./tim run flask db upgrade
. - Käynnistä TIM-palvelin uudelleen.
Olemassa olevan taulun muuttaminen (esim. sarakkeiden lisäys/poisto):
- Tee tarvittavat muutokset malliluokkaan.
- Jatka luomisohjeen kohdasta, jossa ajetaan
./tim run flask db migrate
.
3.2.2 Dokumenttien käsittely
Olennaisimmat luokat:
DocEntry
jaDocInfo
- Dokumentin haku kannasta
- Uuden dokumentin luonti
- Dokumentin metatiedot (mm. omistaja, polku, käännökset, otsikko)
Document
- Dokumentin sisällön käsittely
- Lohkojen haku, lisäys, poisto
- Dokumentin vienti tekstimuotoon (Manage-näkymä)
DocParagraph
- Yksittäisen lohkon käsittely
3.3 Esimerkki
Määritellään reitti, joka tulostaa valitun dokumentin markdown-sisällön. Käyttäjällä tulee olla edit-oikeus dokumenttiin.
@app.get("/getMd/<path:doc_path>")
def print_markdown(doc_path: str) -> Response:
"""Returns the paragraphs as plain markdown for the specified document.
:param doc_path: The document path.
:return: The document markdown.
"""
# Search for the document in the database based on its path. We also search the translations.
d = DocEntry.find_by_path(doc_path, try_translation=True)
# If the document is not found, an error is returned.
if not d:
raise NotExist()
# Ensure that the user has edit access to this document. If not, we abort
# the request and return an error message.
verify_edit_access(d)
# Define a generator that assembles the response content piece by piece.
# This way we don't have to construct any long string in memory.
def generate_response():
for p in d.document.get_paragraphs():
yield f"{p.get_markdown()}\n\n"
# Return the response as plain text using the above generator.
return Response(generate_response(), mimetype="text/plain")
3.4 Suorituskyvyn debuggaus
Jos halutaan selvittää, mihin reitin suorittamisessa kuluu aikaa, voi asettaa konffioption PROFILE = True
. Tällöin kunkin reitin suorittamisen yhteydessä:
- konsoliin tulostuu tietoa suoritusajoista
- hakemistoon
<repo>/profiling
syntyy tiedosto<uniikkinimi>.prof
Konsolituloste ei välttämättä anna selkeää kokonaiskuvaa. Siksi vastaavan .prof
-tiedoston voi avata jollakin analysointiohjelmalla, kuten SnakeVizillä.
- Asenna SnakeViz koneeseesi:
pip install snakeviz
- Avaa prof-tiedosto:
snakeviz <tiedosto>
SnakeVizin kaavio on interaktiivinen, eli hiirtä liikuttamalla näkee kohdalla olevan funktion tietoja ja klikkaamalla voi zoomata siihen.
4. Selainpuolen kehitys
4.1 HTML-muotit
HTML-muotteja on kahdenlaisia:
- Palvelinmuotti: muotissa esiintyvät muuttujat korvataan palvelimella. Palvelinmuotit sijaitsevat hakemistossa timApp/templates. Näiden käyttöä tulee välttää, koska ne eivät ole kovin joustavia.
- Selainmuotti/Angularmuotti: nämä prosessoidaan selaimen päässä. Suurin osa näistä on kirjoitettu suoraan Angular-komponenttien yhteyteen samaan TypeScript-tiedostoon. Pieni osa vanhoista AngularJS-muoteista on hakemistossa timApp/static/templates.
Palvelinmuotit voidaan edelleen jakaa kahteen ryhmään: kokonaisiin ja osittaisiin. Kokonaiset muotit edustavat kokonaista HTML-sivua, eli ne yleensä alkavat <!doctype html>
ja päättyvät </html>
. Osittaiset muotit taas edustavat vain tiettyä osaa sivusta, ja niitä voi sisällyttää muihin muotteihin. Osittaiset muotit sijaitsevat hakemistossa timApp/templates/partials.
Kaikki kokonaiset HTML-muotit periytyvät muotista base.jinja2
. Alla olevassa kaaviossa on esitetty kokonaisten muottien perimähierarkia.
4.1.1 Muottien merkitykset
- settings: Asetusnäkymä.
- start: Etusivu.
- view_html: Dokumenttinäkymä.
- show_slide: Dokumentin kalvonäkymä.
- manage: Hakemiston tai dokumentin hallintanäkymä.
- index: Hakemistonäkymä.
- duration_unlock: Objektin lukitusnäkymä, joka näkyy, kun oikeusaika on mennyt umpeen tai se ei ole vielä alkanut.
- Muut kokonaiset muotit ovat "abstrakteja", eli jonkun muotin täytyy aina periä se.
4.2 TypeScript
TIM käyttää selainpuolen kehityksessä JavaScriptin sijasta TypeScriptiä, joka käännetään JavaScriptiksi.
Kaikki JavaScript on validia TypeScriptiä, mutta TypeScript mahdollistaa koodin tyypityksen ja sitä kautta helpomman ylläpidon ja kehityksen.
Vältä any
-tyypin käyttöä ja as
-tyyppimuunnoksia sekä @ts-ignore
-merkintää.
4.2.1 Työnkulku
Aluksi TIMin kääntämisen yhteydessä ajettava ./tim npmi
asentaa tarvittavat kolmannen osapuolen kirjastot.
Tämän jälkeen ajetaan timApp
-hakemistossa (tai IDE:n kautta) npm run bdw
, joka kuuntelee skripteihin tulleita muutoksia ja kääntää ne aina uudelleen.
4.2.2 Tuotannossa
Mitä pitää tehdä tuotantokoneessa, jos tekee git pull
ja .ts
-tiedostot muuttuvat?
- Yleensä riittää:
./tim js
- Jos ulkoiset kirjastot päivittyvät, niin:
./tim update front
4.2.3 Kirjastojen asennus
Uuden JS-kirjaston asennus vaatii monesti TypeScript-tyyppien asentamisen erikseen. Hakukone tyypeille löytyy TypeScriptin kotisivuilta. Siis timApp
-hakemistossa:
npm i --save KIRJASTO
npm i --save-dev @types/KIRJASTO
Komentojen ajaminen muuttaa tiedostoja package.json
ja package-lock.json
. Commitoi muutokset Gittiin.
4.3 AngularJS ja Angular
TIMin selainpuoli on alun perin toteutettu AngularJS-kirjastolla. Joulukuussa 2019 otettiin käyttöön uuden Angularin (Angular 9:n) koontisysteemi (virallisesti Angular CLI). Tämän johdosta uusia käyttöliittymäkomponentteja ei enää pidä kirjoittaa vanhaa AngularJS:ää käyttäen. Olemassa olevat komponentit on tarkoitus vähitellen muuntaa uuteen Angulariin.
Uusi Angular tarjoaa 2 tapaa siirtyä vanhasta AngularJS:stä pois.
TIMissä on tarkoitus käyttää jälkimmäistä downgrade-tapaa, koska sen avulla saa jo varhaisessa vaiheessa suorituskykyhyötyjä uudesta Angularista.
HUOM: Kun luot uuden TypeScript-tiedoston, siinä olevat tim-importit eivät toimi ennen kuin importtaat sen johonkin muuhun tiedostoon (mikä pitää tietenkin aina ennen pitkää tehdä). Tämä johtuu siitä, että sellaiset ts-tiedostot, joita ei importata mihinkään, eivät ole mukana projektissa. Eli kaikkien ts-tiedostojen on oltava "saavutettavissa" main.ts-tiedostosta alkaen.
Uusien Angular-komponenttien, -direktiivien, -moduulien ja -palvelujen tiedostonimet tulee olla muotoa
joku-nimi.{component,directive,module,service}.ts
Muista, että TIM-ohjeissa (ja muuallakin netissä):
- Sanalla Angular tarkoitetaan uutta Angularia (>= v2).
- Sanalla AngularJS tarkoitetaan vanhaa AngularJS:ää (<= v1.8).
4.4 CSS ja SASS/SCSS
Uusien Angular-komponenttien tyylit tulee määritellä komponenttikohtaisesti kuten Angularin ohjeessa kerrotaan.
Uuden Angularin myötä globaaleista tyyleistä (tarkoittaa lähes kaikkia stylesheet
-kansion tiedostoja) tulee pyrkiä eroon. Lopullinen päämäärä on, että globaaleja tyylitiedostoja ovat vain
- "pääteematiedosto", jossa luetellaan TIMin oletusvärit, fontit, yms.
- teematiedostot, jotka ylikirjoittavat osan pääteematiedoston asetuksista
ja näissä on vain muuttujien esittelyitä, ei varsinaista (S)CSS:ää. Komponenttien tyylitiedostoissa sitten viittaillaan näihin muuttujiin.
4.4.1 Teematiedostot ja Bootstrap, vanha tapa
Tämä osio koskee vanhoja AngularJS-komponentteja ja muuta TIMin HTML:ää, jotka eivät ole Angular-komponentteja.
TIM käyttää SASS-esikääntäjää. Sen avulla esimerkiksi värit voidaan määritellä yhdessä paikassa niin, että värien muuttaminen ei vaadi muutosta moneen kohtaan. Näiden muuttujien oletusarvot on määritelty tiedostossa variables.scss
.
TIMissä on tietty määrä teematiedostoja, joiden sisältö tulee olla alla olevan mukainen. Oletetaan, että tiedoston nimi on teema.scss
.
@charset "UTF-8";
/* Tämä on esimerkkityylitiedosto. */
// muuttujat
$basic-color: green;
// seuraava mixin ei ole pakollinen
@mixin teema {
// tähän tavanomaista (S)CSS:ää, joka ylikirjoittaa oletustyylit
}
Käytössä on myös Bootstrap-kirjasto (versio 3) ja siihen liittyvä UI Bootstrap-kirjasto. Oman CSS:n kirjoittamista tulee välttää ja sen sijaan käyttää Bootstrapin ja UI Bootstrapin tarjoamia tyyliluokkia ja komponentteja.
Bootstrap 3 on kuitenkin jo vanha kirjasto, joten elementtien asemoinnin voi hoitaa myös flexbox-tyyleillä. Tätä varten on TIMiin tehty muutamia omia tyyliluokkia, joita voi tarvittaessa tehdä lisää.
Joskus tulevaisuudessa paras asemointityökalu on CSS Grid, jahka IE:stä päästään eroon.
Eri asioiden tyylit on lajiteltu omiin SCSS-tiedostoihinsa. Kaikki tyylit kootaan yhdeksi all.scss
-tiedostossa.
4.5 Angular-komponenttien kääntäminen muille kielille
Kirjoita selainpuolen koodin tekstit vain englanniksi ja sen jälkeen merkitse tarvittavat tekstit käännettäväksi. Lopuksi generoi käännöstiedosto extract-i18n
-skriptillä ja muuta käännökset.
Selainpuolen tekstien merkitseminen käännettäväksi onnistuu kahdella tavalla: merkitsemällä Angular-templatet ja merkitsemällä mielivaltaiset merkkijonot.
4.5.1 Angular-templateiden merkitseminen käännettäväksi
Merkitse templatessa teksti käännettäväksi i18n
-attribuutilla, esim:
Jos teksti on attribuutissa eikä itse elementin sisällä, se onnistuu myös:
Siis aina i18n-<attribuutin nimi>
.
Huom: vältä liian laajojen lohkojen ottamista käännettäväksi. Esimerkiksi
ei ole hyvä merkintä, koska silloin käännöstekstiin otetaan mukaan myös <input>
-elementti.
Sen sijaan voit käyttää <ng-container>
tai muita elementtejä hyödyksi. Esimerkiksi edellistä versiota parempi vaihtoehto on
jolloin käännökseen otetaan vain teksti Some checkbox
.
4.5.2 Mielivaltaisen tekstin merkitseminen käännettäväksi
$localize
-komennolla voi määrittää mielivaltaiset käännettävät merkkijonot. Esimerkiksi
Jos tekstissä on mukana muuttujia (esim. numeroita), voi käyttää template-merkkijonojen muuttujasyntaksia:
Sen käyttämisestä on jonkun verran Angularin ohjeissa:
https://angular.io/api/localize/init/$localize
4.5.3 Merkittyjen tekstien kääntäminen
- Siirrä kääntämättömät tekstit käännöstiedostoihin ajamalla npm-skripti
extract-i18n
(IDEssä tai komentorivillä timApp-kansiossanpm run extract-i18n
). - Muokkaa käännöstiedostoja (jotka ovat hakemistossa
timApp/i18n
) haluamallasi tavalla. Esimerkiksi Poedit on helppo käyttää. Jos lähdetekstin alussa tai lopussa on välilyöntejä tai muita tyhjiä merkkejä, pidä ne käännöksessäkin mukana. - Jos olet kehitysmodessa (
IS_DEVELOPMENT=true
) aja npm-skriptibdlw
. Testaa selaimessa, että käännökset näyttävät järkeviltä. Tätä varten lokaalin TIMin asetussivulla kannattaa olla kielivalinta "Use web browser preference", jolloin voit jollakin selainliitännäisellä (esim. Quick Accept-Language Switcher) vaihtaa nopeasti kieltä. Huomaa, että kielen vaihtaminen vaatii aina sivun kokonaan virkistämisen. - Ei-kehitysmodessa (
IS_DEVELOPMENT=false
)./js
riittää.
Huom: bdlw
on toistaiseksi melko kömpelö, koska sen joutuu usein ajamaan kokonaan uudelleen, vaikka se näyttäisikin kuuntelevan muutoksia.
4.6 TIMin käyttöliittymäkomponentit
Valmiiden Angular-komponenttien lisäksi TIMiin on tehty muutamia omia komponentteja. Alla on listattu näistä muutamia.
4.6.1 Bootstrap panel
- Paneeli, jolla on otsikko, jonka voi sulkea ruksista ja jonka sisään voi laittaa mitä tahansa.
- Esimerkki: Etusivun create document -kohta.
- Direktiivi:
bootstrap-panel
.
4.6.2 Rights editor
- Komponentti, jolla voi editoida jonkin objektin oikeuksia
- Esimerkki: Manage-sivun permissions-välilehti.
- Direktiivi:
tim-rights-editor
.
4.6.3 Lomakkeet
Lomakkeissa kannattaa käyttää seuraavia apudirektiivejä:
tim-error-state
: Asettaahas-error
-luokan päälle elementtiin, josfor
- attribuutin mainitsemassa kentässä on virhe.tim-error-message
: Näyttää virheilmoituksen, josfor
- attribuutin mainitsemassa kentässä on virhe.- Esimerkki käytöstä: createItem.html
Lomakkeita varten voi tarvittaessa luoda omia tarkistimia, jotka tarkistavat, onko syöte oikeassa muodossa. Esimerkkejä nykyisistä tarkistimista:
tim-short-name
: Tarkistaa, että merkkijono on validi lyhytnimi (esim. ei saa sisältää välilyöntejä).tim-location
: Muuten kuintim-short-name
, mutta sallii lisäksi/
-merkit.
Omiin tarkistimiin liittyvät virheviestit pitää lisätä formErrorMessages.ts-tiedostoon.
5. Tyypeistä
Timin koodissa pyritään hyödyntämään koodin tyypitystä ajonaikaisten virheiden vähentämiseksi ja koodin ylläpidon helpottamiseksi. Luvussa kerrotaan oleellisimmat asiat liittyen tyypityksiin selain- ja palvelinpään koodissa.
5.1 Käännösaikainen tyypitys
Käännösaikaisen tyypityksen idea on, että mahdollisimman moni tyyppivirhe tulisi ilmi jo ennen koodin ajamista.
Selainpuolella käännösaikaisessa tyypityksessä on käytössä TypeScript. Sen strict-asetus on päällä, mikä tarkoittaa, että null
- ja undefined
-arvot on muistettava huomioida. Lisäksi joitakin ESLintin sääntöjä on päällä (näistä kaikki ei liity nimenomaan tyypitykseen).
Palvelinpuolella Pythonissa on käytössä Mypy, joka tavallaan on "Pythonin TypeScript". Ongelmana on, että suurin osa Timin Python-koodista on vielä kunnolla tyypittämätöntä. Tiedostossa mypy.ini
on listattuna kaikki ne tiedostot, joiden osalta Mypy on pois käytöstä. Tällöin kaikissa uusissa tiedostoissa (moduuleissa) Mypy on oletuksena päällä, ja vähitellen vanhoja moduuleja voi korjailla niin, että Mypy hyväksyy ne. Tiedostoon mypy.ini
ei siis pitäisi koskaan luetella uusia tiedostoja.
5.2 Ajonaikainen tyypin validointi
Monesti ongelmana on, että jokin olio on peräisin käyttäjän syötteestä, eikä siksi voida olla varmoja, onko se halutuntyyppinen. Tällöin vääräntyyppisestä syötteestä halutaan antaa selkeä virheilmoitus, tai joskus vain sivuuttaa virheellinen syöte ja käyttää oikeantyyppistä oletusarvoa.
5.2.1 Selainpuoli
Selainpuolella ajonaikaiseen validointiin käytetään io-ts-kirjastoa. Linkin takaa löytyy taulukko TypeScript-tyyppien sekä io-ts-tyyppiolioiden vastaavuuksista.
Oletetaan, että io-ts-kirjasto on importattu lauseella import * as t from "io-ts";
(kuten Timissä onkin kirjaston ohjetta mukaillen tehty). Tällöin esimerkiksi sen tarkistaminen, onko olio merkkijono, onnistuu näin:
declare const something: unknown; // jokin käyttäjän syötteestä tuleva arvo
if (t.string.is(something)) {
console.log(something.startsWith("x")) // ei tyyppivirhettä
}
Yllä olevassa koodissa console.log
-rivillä ei ole tyyppivirhettä, koska metodin t.string.is(x: unknown)
paluuarvon tyyppi on x is string
, mikä ilmaisee TypeScriptille, että metodin paluuarvo kertoo, onko olio string
-tyyppinen vai ei. Jos if-lause on tosi, niin TypeScript tietää, että something
on merkkijono ja sallii startsWith
-metodin kutsumisen.
io-ts-kirjaston vahvuus tulee siitä, että tyyppejä voi luoda kombinaattorien avulla itse miten monimutkaisia tahansa.
5.2.2 Palvelinpuoli
Palvelinpuolella (Python) validointiin käytetään marshmallow_dataclass-kirjastoa. Tyypit esitellään dataclass
-luokkina ja niille luodaan validaattori funktiolla class_schema
. Kirjastolla marshmallow_dataclass
on myös oma dataclass
-dekoraattori, jonka avulla class_schema
:a ei tarvitse itse kutsua, mutta sitä ei (ainakaan vielä) kannata käyttää, koska Mypy ei ymmärrä sitä tarkistaessaan tyyppejä, vaan antaa virheilmoituksia.
5.2.2.1 Flaskin reitit
Flaskin reitteihin tulevien parametrien validointiin on olemassa 2 tapaa:
Tapa 1 (uusi, suositeltu): Käytä uusissa reittikokoelmissa TypedBlueprint
-oliota. Sen avulla reittiin tulevat parametrit validoidaan suoraan reittifunktion esittelyrivin perusteella, eli erillistä dataclass-luokkaa ei tarvitse luoda.
Jos jokin parametri ei ole pakollinen, anna sille oletusarvo. Jokaisella parametrilla on oltava tyyppiannotaatio (jos se puuttuu, siitä tulee selkeä poikkeus heti palvelimen käynnistyksessä).
Tavallisten Blueprint
tien reitteihin voi yksitellen lisätä tuen edellä mainituille esittelyrivityylisille parametreille laittamalla @xxx.route(...)
-rivin jälkeen @use_typed_params(luku)
, joka tarvitsee parametrikseen tiedon siitä, kuinka monta Flaskin URL-muuttujaa reitissä on. Esimerkiksi reitissä @xxx.route('/joku/<eka>/<toka>')
olisi 2 URL-muuttujaa, jolloin reittifunktion loput parametrit otetaan URL-parametreista (?a=b
jne) tai selaimen antamasta JSON-oliosta.
Tapa 2 (vanha tapa, ei suositeltu) on käyttää dekoraattoria @use_model(Luokka)
, jossa on kaksi huonoa puolta:
- Jokaiselle reitille joutuu kirjoittamaan dataclass-luokan.
- Et voi käyttää Flaskin URL-muuttujia niiden kanssa.
5.2.2.2 Pohdintaa
Monesti halutaan hyväksyä olio, jolla on tietyn tyyppisiä attribuutteja, mutta joita kaikkia ei ole pakko antaa. Tällaisia voivat olla esim. asetusobjektit (kuten pointsumrule) tai Flask-reitin URL-parametrit. Tällöin dataclassiin voi antaa sellaisille oletusarvon. Esimerkiksi:
@dataclass
class SearchParams:
text: str
max_results: int = 10
SearchParamsSchema = class_schema(SearchParams)
jota voitaisiin käyttää näin:
syote = {'text': 'hei'}
try:
params: SearchParams = SearchParamsSchema().load(syote)
except ValidationError as e:
# TODO: Käsittele virhe asianmukaisesti tilanteesta riippuen.
# Jos ollaan Flask-reitissä, niin tätä try-exceptiä ei tarvitsisi, vaan
# suoraan `use_model(SearchParams)` funktion yläpuolelle.
return
print(params.max_results) # tulostaa 10
Yllä olevassa koodissa validoinnin jälkeen params
-oliosta ei voi nähdä, antoiko käyttäjä max_results
in syötteeksi saman kuin oletusarvon (10
) vai jätettiinkö kenttä antamatta. Tämä ei välttämättä ole ongelma yllä olevassa koodissa, mutta joskus tämä tieto halutaan säilyttää.
Jos siis halutaan saada tieto siitä, mitä kenttiä käyttäjä ei ole antanut, voidaan käyttää tyyppiä Union[X, Missing]
, jonka oletusarvoksi laitetaan missing
:
@dataclass
class SearchParams:
text: str
max_results: Union[int, Missing] = missing
Miksi sitten ei voisi olla näin:
@dataclass
class SearchParams:
text: str
max_results: Optional[int] = None
Siksi, että null
on mahdollinen arvo JSONissa, eli nyt ei taas tiedettäisi varmasti, antoiko käyttäjä arvon itse vai ei. Mutta taas tapauksesta riippuen Optional
toimii toki, jos tällaista tietoa ei tarvita.
5.3 Tyyppien vertailu
Seuraavassa taulukossa esitetään TypeScript-, io-ts- ja Python-tyyppien vastaavuuksia esimerkkien avulla.
TODO: Tästä puuttunee vielä ohje omien tyyppien tekoon ja niistä interface tekoon
—Taulukossa on joitakin erikoismerkintöjä:
~
tarkoittaa, ettei tyypille ole täydellistä vastinetta, vaan tarjotaan lähimpänä oleva.-
tarkoittaa, ettei tyypille/kombinaattorille ole mitään käyttökelpoista vastinetta.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.