# timOhjeet

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

18 Aug 23 (edited 21 Aug 23)

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 :)

25 Aug 23 (edited 25 Aug 23)
G clusterVirtuaaliKone (virtuaali)kone clusterPlugins liitännäiset browser Selain caddy Caddy browser->caddy csPlugin csPlugin tim TIM csPlugin->tim showFile showFile showFile->tim haskellplugins haskellplugins haskellplugins->tim tim->csPlugin tim->showFile tim->haskellplugins Dumbo Dumbo tim->Dumbo tim->caddy Dumbo->tim caddy->browser caddy->tim

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 ja plugin
  • 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.

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ä ja
  • yksi.

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 ja huomautus
  • attribuutteja ovat a ja c, joiden arvot ovat vastaavasti b ja d.
  • t on MHg4ZjM2YzRk (tämä on aina piilossa käyttäjältä)

Lohkon talletusmuoto on seuraavanlainen JSON:

{
    "md": "Terve!",
    "id": "AiKFDMMCQ9gS",
    "t": "MHg4ZjM2YzRk",
    "h": {
        "LTB4NTEzYzFkZTk=": "<p>Terve!</p>"
    },
    "attrs": {
        "classes": ["red", "huomautus"],
        "a": "b",
        "c": "d"
    }
}

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.

24 Aug 23 (edited 24 Aug 23)

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ä:

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 puuttuu
  • NotExist kun jotakin objektia ei löydy
  • RouteException 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: Kuten verify_view_access, mutta edit-oikeudelle.
  • verify_manage_access: Kuten verify_view_access, mutta manage-oikeudelle.
  • verify_seeanswers_access: Kuten verify_view_access, mutta see answers -oikeudelle.
  • verify_teacher_access: Kuten verify_view_access, mutta teacher-oikeudelle.
  • verify_ownership: Kuten verify_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 muodossa docId.taskId, missä docId on dokumentin tunnistenumero ja taskId 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 ja DocInfo
    • 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.

SnakeVizin interaktiivinen kaavio
SnakeVizin interaktiivinen kaavio

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.

G item item base base item->base folder folder folder->item document document document->item duration_unlock duration_unlock duration_unlock->item manage manage manage->item view_html view_html view_html->document show_slide show_slide show_slide->view_html settings settings settings->base start start start->base index index index->folder

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:

<p i18n>Hello world!</p>

Jos teksti on attribuutissa eikä itse elementin sisällä, se onnistuu myös:

<p title="Hello world!" i18n-title></p>

Siis aina i18n-<attribuutin nimi>.

Huom: vältä liian laajojen lohkojen ottamista käännettäväksi. Esimerkiksi

<label i18n>
  <input type="checkbox"> Some checkbox
</label>

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

<label>
  <input type="checkbox"> <ng-container i18n>Some checkbox</ng-container>
</label>

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

$localize`Keyboard mode` 

Jos tekstissä on mukana muuttujia (esim. numeroita), voi käyttää template-merkkijonojen muuttujasyntaksia:

// Jossakin koodissa määritelty messageCount-muuttuja.

$localize`You have ${messageCount} unread messages`;

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-kansiossa npm 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-skripti bdlw. 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

Bootstrap panel
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

Rights editor
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: Asettaa has-error-luokan päälle elementtiin, jos for- attribuutin mainitsemassa kentässä on virhe.
  • tim-error-message: Näyttää virheilmoituksen, jos for- 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 kuin tim-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 Blueprinttien 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_resultsin 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

16 Feb 23

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.