# timOhjeet

Arvostelu JavaScriptillä

Aina ei riitä yksinrtainen pisteytys tyyliin: "Jos vastauksessa lukee kissa, anna 1p". Tällöin voidaan turvautua csPlugin ja/tai JsRunnerin voimaan. Välimuotona voidaan pluginiin kirjoittaa arvostelun hoitava JavaScript-koodi.

1. Ensimmäinen esimerkki

Olkoon meillä tehtävä, jossa pitää kirjoittaa kaksi merkkijonoa, joista jälkimmäisen ollessa ensimmäistä pidempi, saadaan 1p.

# e1

Meni rikki, kun ajoin. Vai oliko tarkoitus dumpata kasa debug-tulostetta?

DZ: Joo, tarkoituksena on nähdä, mitä kaikkea dataa JavaScript-koodi saa käyttöön.

26 Jul 21 (edited 29 Dec 21)

Tämä on tehty koodilla:

``` {#e1 dquestion="true" plugin="qst"}
answerFieldType: inputText
answerLimit:  
expl: {}
headers:
- ''
matrixType: textArea
questionText: Jonot
questionTitle: e1
questionType: matrix
size: "15"
rows:
- '1. jono a:'
- '2. jono b:' 
postprogram: |!!
  function points(a,b) {
      print(a + "," + b + "\n");  // tulostetaan debhuggausta varten.  Oikeasti ei.
      if (a.length > 0 && a.length < b.length) return 1;
      return 0;
  }
  print("\<pre\>" + JSON.stringify(data, null, 2) + "\</pre\>");  // debuggausta varten
  let matrix = data.save_object;
  data.points = points(matrix[0][0], matrix[1][0]);
  return data;
!!  
stem: "Kirjoita kaksi jonoa, joista jälkimmäinen on pidempi"
```
# kentatkaytossa

2. Käytössä olevat kentät

Funktiota postprogram kutsutaan tallennusvaiheen lopussa ja se saa käyttönsä seuraavan tietueen:

data = { 'points': points,
          'save_object': save_object,
          'tags':       tags,
          'is_valid':   is_valid,
          'force_answer': force_answer,
          'allow_save': true,
          'refresh': false,
          }
  • save_object: tallennukseen menevä tietojen raakamuoto. Se mitä tämä sisältää, pitää tutkia jokaista plugintyyppiä varten erikseen. Helpoiten tämä onnistuu kun pistää koodin alkuun kehityksen alkuvaiheessa tulostuslauseen:

      print(JSON.stringify(data)+"\n");

    voi kokeilla myös

      println("md:```\n" + JSON.stringify(data, null, 4) + "\n```");

    niin saa paremmin muotoiltua tulosta.

  • tags: ???

  • is_valid: tallennetaanko tieto "laillisena" vai ei

  • force_answer: pakotetaan tallennus vaikka sama data olisi jo tallessa

  • allow_save: tallennetaanko vastausta lainkaan (edes invalid)

  • refresh: pakotetaanko tehtävä latautumaan uudestaan vastauksen jälkeen. Kaikki postprogram-funktiossa tehdyt vastauksen muutokset eivät välttämättä näy tehtävässä ennen tehtävän tai sivun uudelleenlatausta

Funktio voi muuttaa näitä arvoja ennen saman datan palauttamista. Voi muuttaa jopa käyttäjän vastausta, mutta käyttäjä näkee sen vasta seuraavalla sivunlatauksella ellei koodissa ole pakotettu tehtävän uudelleenlautumista laittamalla data.refresh = true.

Alkeellista ohjetulostetta saa käyttäjälle toistaiseksi käyttämällä tuota debuggaukseen käytettyä print-lausetta.

Itse pääohjelman pitää päättyä aina

return data;

Oletuksena print-lauseella tulostettu teksti tulostuu komponentin alapuolelle info-laatikkoon, jonka käyttäjä voi sulkea.

Tuloste voidaan ohjata myös menemään eri paikkaan käyttäen attribuuttia

postoutput: topfeedback   # tulee komponentin yläpuolelle
postoutput: error         # yläpuolelle hälytyslistaan 
postoutput: web.error     # pluginin error osaan, toiminta riippuu pluginista

3. Sisällön hakeminen eri pluguintyypeissä

Seuraavassa esimerkkejä eri plugineista. Katso lähdekoodi "View Source" -toiminnolla.

3.1 csPlugin

csPluginissa käyttäjän syöte ja esim ohjelman tuloste saadaan

let koodi = data.save_object.usercode;
let input = data.save_object.userinput;
let args = data.save_object.userargs;
let output = data.web.console;
let error = data.web.error;

Tässä tulos voidaan laittaa myös konsoliin:

data.web.console = palaute;

tai lisätä se tarvittaessa ohjelman tulostaman perään

data.web.console += "\n" + palaute;

Tai sama saadaan aikaiseksi myös ilmoittamalla tulostuksen kohde attribuutilla

postoutput: web.console
# shell

3.2 fields

Fieldeissä sisältömerkkijono on yleensä

let s = data.sava_object.c;
# numex

Please to interact with this component.

{}

Please to interact with this component.

{}

Please to interact with this component.

{}

# cbex

Please to interact with this component.

3.3 qst-monivalinnat

qst-monivalinnoissa tieto on 2-ulotteisessa talukossa, jossa riveillä on ruksien tapauksessa riveiltä valittujen kohtien numerot. Yhden sarakkeen "taulukoissa" sarake edustaa riviä.

let s = data.save_object;
# qstex
# elex

3.4 TimTable

TimTablesta käyttäjän vastaus saadaan oliona:

let cells = data.save_object.userdata.cells

Tämän muoto on esimerkiksi:

{"B2":"musta","C5":"harmaa","D5":{"backgroundColor":"#FFFF00","cell":"keltainen"}}

Eli tuosta lukea mitä arvoja käyttäjä on muuttanut oletusarvoihin verrattuna. Yhden solun kohdalla voi olla joka solun sisältö tai jos myös sen attribuutteja (värejä yms) on muutettu, niin niiden arvot.

# timtableex
# kissaex

4. Extrafiles esimerkki JavaScriptillä

csPluginin ohjeissa on esimerkki Arvostelu extrafiles avulla. Tehdään vastaava esimerkki jsrunnerilla.

Esimerkki, jossa pitäisi antaa joku kissaeläimen nimi ja sitten saa pisteitä seuraavasti:

- tiikeri: 0.8 p
- kissa: 0.5 p
- muut 0 p

ja noista kustakin tulee joku palaute:

# lueKissa

Koodi, jolla em. plugin on tuotettu:

``` {#lueKissa plugin="csPlugin"}
type: text
placeholder: "kirjoita joku kissaeläimen nimi tähän"
postprogram: |!!
   // print(JSON.stringify(data)+"\n");  // debuggausta varten
   let lines = data.save_object.usercode;
   // print(lines);
   let pisteet = 0;
   let palaute = "";
   if (lines.match(/.*tiikeri.*/)) {
       pisteet = 0.8;
       palaute = "Tämä oli hyvä";
   } else if (lines.match(/.*kissa.*/)) {
       pisteet = 0.5;
       palaute = "Aika tavallinen";
   } else {
       palaute = "En tunne tätä kissaeläimeksi!";
   }
   data.points = pisteet;
   print(palaute);
   return data;
!!        
```

5. Esimerkki jossa ohjelman antamia pisteitä vähennetään

Oletetaan että meillä on tehtävä, jossa pitää muuttaa ohjelma niin että siinä olevaa tulostetta on muutettava. Kääntyvästä ohjelmasta saa kaksi pistettä, mutta jos koodissa on edelleen "Hello" edes osana tai kokonainen pelkkä sana "world", niin vähennetään piste.

# Plugin1

Laskentakoodi (kokonaisuudesta katso lähdekoodi):

postprogram: |!!
    let illegals=[/hello/i, /\bworld\b/i];
    
    let code = data.save_object.usercode;
    let minus = 0;
    for (let w of illegals) 
        if (m = code.match(w)) { print("Koodissa esiintyy " + m[0] + "\n"); minus++; }
    minus = Math.min(minus, 1);
    if ( minus > 0 ) print("Vähennetään " + minus + " pistettä!\n");
    
    data.points = Math.max(data.points-minus, 0);
    return data;
!!

6. Ulkoisia JavaScript-kirjastoja

Jos koodi on kirjoitettu ulkoiseen JS-tiedostoon, voidaan se ottaa mukaan attribuutilla postlibraries:

postlibraries: 
  - https://tim.jyu.fi/csstatic/dfa/vars.js
postlibrariesEdit:
  0:
    deleteAfter: // ------------------ Variables END

Attribuutilla postlibrariesEdit voidaan antaa joukko muokkauskomentoja, jolla kirjastoa muokataan ennen runnerille antamista. Usein tällä poistetaan esimerkiksi JavaScript modulia varten oleva jono

export {funktio1, funktio 2};

koska runnerin käyttämä JavaScript ei tätä tunne.

# rndtasks

7. Arvottuja tehtäviä

Katso arvonnasta lisää dokumentista Satunnaistus

7.1 Perusmalli

Satunnaistus ei oikeastaan tuo tilanteeseen muuta uutta kuin, että arvostelussa pitää ehkä laskea vaadittava tulos arvonnan perusteella. Seuraavassa tämä esimerkkinä yhteenlaskuja kyselevästä versiosta.

# summa

Tämä on tehty koodilla:

``` {#summa plugin="csPlugin" rnd="[[-10,10],[-10,-1]]"}
type: text/tiny
stem: "Laske: %%rnd[0]%% +  %%rnd[1]%%"
postprogram: |!!
 // print(JSON.stringify(data)+"\n");  // debuggausta varten
 let a = %%rnd[0]%%;
 let b = %%rnd[1]%%;
 let c = a + b;
 let u = parseInt(data.save_object.usercode);
 let t = "väärin!";
 if (c === u) {
   t = "OK";
   data.points = 1;
 }  
 print(`${a} + ${b} = ${c}; ${u} on ${t}`);
 return data;
!!
```
# rndrepeat

7.2 Useita samankaltaisia tehtäviä toistuvasti

Mikäli halutaan sellainen harjoittelukomponentti, jolla voi tuottaa jatkuvasti uusia tehtäviä, voidaan lisätä pluginin otsikkoriville

seed="answernr"

jolloin siemenluku riippuu siitä monesko vastauskerta on menossa. Jokaisen vastauksen jälkeen voidaan pyytää uusi tehtävä, mutta uutta tehtävää ei saa ennen kuin entiseen on vastattu jotakin. Vanhoja tehtäviä ja vastauksia voi selata ja kokeilla uudelleen

Uuden tehtävän tuottavan painikkeen tekstin voi vaihtaa:

buttonNewTask: Uusi tehtävä

Näin tehty kysymys sopii parhaiten sellaiseen, missä pisteillä ei ole merkitystä, vaan harjoitellaan jotakin tiettyä tehtävätyyppiä niin kauan kuin haluaa. Toki pisteitäkin voi käyttää ja silloin oletuksena viimeisen tehdyn tehtävän pisteet jäävät voimaan. Alempana on esimerkki miten postprogram-ohjelmalla voidaan tehdä sellainen, missä pitää kerätä 3 onnistunutta peräkkäistä suoritusta jotta saa pisteet.

Oletuksena uusiinkiin tehtäviin tulee viimeisin vanha vastaus. Jos tämän haluaa muuttaa niin, että uusiin tehtäviin tulee tyhjä vastauspohja voi lisätä attribuutin:

initNewAnswer: ""

Jos tämän lisäksi haluaa tietyn pohja-alustuksen jokaiselle uudelle tehtävälle, voi laittaa csPlugin-tehtävissä esimerkiksi:

fullprogram: "123"
# summa2
# removesaved

7.2.1 Saved-tekstin poisto

Monissa plugineissa vastaaminen tuottaa esim tekstin Saved. Tämä voi olla hämäävää, koska jos vastataan "vanhaan" kysymykseen uudelleen, uutta vastausta EI tallenneta eikä siihen vastaaminen muuta pisteitä. csPlugin-pohjaisissa tehtävissä voi laittaa attribuutin:

savedText: ""

jolloin ko tekstiä ei näy koskaan. Jos csPlugin pohjaisissa tehtävissä halutaan kuitenkin näyttää teksti oikean tallennuksen yhteydessä, mutta ei muulloin, voidaan laittaa postprogram-ohjelmaan:

if (!data.answer_call_data.info.askNew) data.web.error = ""; 

Toki pitäisi myös tarkistaa että tuo error teksti on vain tuo Saved, muuten hukataan mahdollinen virheviesti. Ja tarvittaessa tietysti voisi laittaa muunkin tekstin silloin kun vastataan vanhoihin tehtäviin.

# teekolme

7.3 Täytyy saada monen tehtävän vastaus oikein

Seuraava muutos edelliseen esimerkkiin voisi olla sellainen, että pitää saada kolme peräkkäistä vastausta oikein, jotta saa yhden pisteen ja tämän jälkeen pisteet eivät muutu. Esimerkin perusteella koodia muokkaamalla voi mielikuvituksen rajoissa tehdä lähes mitä vaan. Esimerkki antaa lisäpisteitä peräkkäisten oikeiden määrän perusteella tms.

Funktion postprogram data-oliossa on attribuutti state, josta saa edellisen tallennuksen tietoja. Ja koska uusi tallennus menee attribuutin save_object kautta, voidaan näiden välisellä tiedonsiirrolla rakennella mitä vaan.

Katso esimerkin toteutus painamalla Edit-menusta View source.

# summa5

7.4 Erota vastauksen tutkiminen omaksi tiedostokseen

Edellinen esimerkki toimii sellaisenaan. Mutta jos haluaisi tehdä paljon tuollaisia samankaltaisia tehtäviä, niin se kolmen yrityksen testaaminen ja datan palauttamien pitäisi kopioida jokaiseen tehtävään erikseen.

Siksi tuo koodi kannattaa kirjoittaa omaksi funktiokseen ja sitten siirtää omaan tiedostoonsa.

7.4.1 Tutkiminen omaan funktioon

# summa5f

7.4.2 Funktio "kirjastoon"

Luodaan uusi tiedosto, esimerkiksi funktiota/tarkistaonkovalmis.js ja kopioidaan sinne funktion sisältö. Jotta dokumenttia ei muotoilla, lisätään dokumentin asetuksiin

textplain: true

Dokumentilla pitää antaa katseluoikeus (view) anonyymeille käyttäjille.

Sitten lisätään pluginin attribuutteihin esimerkiksi:

postlibraries: 
  - https://tim.jyu.fi/print/tim/ohjeita/funktioita/tarkistaonkovalmis.js
  

Huomaa että tuo on muuten dokumentin osoite, mutta siinä on print, jotta suoritetaan vain pelkkä tekstimuotinen "tulostus" kun tiedosto otetaan mukaan.

# summa5fdoc

Oman tiedostoon kirjoitettavan funktion hyöty on siinä, että sitten kuka tahansa voi sitä käyttää mistä tahansa dokumentista ja näin voi luoda "kirjaston" yleiskäyttöisistä tarkistimista.

7.4.3 Funktio makroon

Toinen tapa tehdä funktiosta yleiskäyttöinen oman dokumenttihierarkian sisällä olisi laittaa dokumentin asetuksiin:

macros:
  tarkistaonkovalmis: |!!
  // Tarkistaa onko jo "tarvitaan" perättäistä oikeata vastausta
  function tarkistaOnkoValmis(oikein, tarvitaan) {
    let state = data.answer_call_data.state;    
    ... muuta funktion rivit
  }     
!!  

ja sitten itse pluginissa kirjoittaa laajennuspyyntö tähän makroon postprogram - funktion alkuun:

postprogram: |!!
 %%tarkistaonkovalmis%%

 let a =...

Jos samaa funktiota tarvitaan hierarkiassa useammassa dokumentissa, voitaisiin makro laittaa sopivaan preamble-dokumenttiin. Kuitenkin jos funktiota tarvitaan useammassa tiedostossa, niin edellisen esimerkin oma dokumentti on suositeltavampi.

# timelimited

8. Tallennus vain tietyn ajan päästä

Jos halutaan tehdä sellainen tehtävä, johon saa vastata vain tietyn ajan päästä edellisestä, voidaan käyttää preprogram attribuuttia hyväksi:

Esiohjelmassa katsotaan onko koskaan tallennettu aikaa. Jos ei, niin jatketaan normaalisti.

Jos on, verrataan kuinka paljon on kulunut edellisesti kerrasta ja estetään tallentaminen ja mahdollinen ajaminen yleensäkin.

Jälkiohjelmassa varmistetaan ettei edes tyhjää oliota tallenneta. Jos tallennus on sallittu, tallennetaan sitä vastaava aika sekunteina seuraavaa yritystä varten:

preprogram: |!!
   // print("\<pre\>" + JSON.stringify(data, null, 2) + "\</pre\>"); // debug
   let raja = 10;  // kauanko pitää olla tallennusten väli
   if (!data.state) return data;  // Ei tallennettu vielä.
   let last = data.state.date;
   if (!last) return data;  // Ei tallennettu vielä.
   let sec = Math.round((new Date()).getTime()/1000) - last;
   if (sec > raja) return data;
   
   print("Viimeinen tallennus " + sec + " sekuntia sitten!<br>");
   print("Et saa vielä yrittää uudestaan!");
   data.markup.nosave = true;
   data.input.nosave = true;
   data.input.type = "text";  // ei aja koodia
   data.markup.savedText = "Ei tallenneta";
   return data;
!!
postprogram: |!!
   // print("\<pre\>" + JSON.stringify(data, null, 2) + "\</pre\>"); // debug
   let save_object = data.save_object;
   if (!save_object || Object.keys(save_object).length === 0) {
      data.allow_save = false; 
      return data;  // koska edellinen ei antanut tallentaa
   }      
   data.save_object.date = Math.round((new Date()).getTime()/1000);
   return data;
!!
# limit1
# timtablecalc

9. Arvojen laskeminen taulukossa

Tehdään "taulukkolaskentaa" vastaava yksinkertainen esimerkki.

# GLO_tuotteet
# runnerLaskeTuotteet
# tuotteet

10. Monta tehtävää joille yhteinen palautteen laskeminen

Tehtävät voit tehdä myös kunkin omaksi fieldikseen ja laskea niille palautteen.

Todo: Tutki toimiiko uudessa dokussa myös 1. kerralla?

Laskutehtäviä

Paljonko on 2+2:

Please to interact with this component.

{}

Please to interact with this component.

{}


Paljonko on 1+2:

Please to interact with this component.

{}

Please to interact with this component.

{}

# runnert1

Kun tehtävät laittaa taulukoon, on tuollaisen ylläpito helpompaa.

TIM: Voisiko samoja makroja mitenkään käyttää kaikissa alueen lohkoissa?

Laskutehtäviä 2

Paljonko on 2+2:

Please to interact with this component.

{}

Please to interact with this component.

{}


Paljonko on 1+2:

Please to interact with this component.

{}

Please to interact with this component.

{}


Paljonko on 3+3:

Please to interact with this component.

{}

Please to interact with this component.

{}


Please to interact with this component.

{}

# runnert2

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.