niedziela, 1 czerwca 2014

Play Formularze - opis ćwiczeń

Poniżej opis drugiego modułu (słowo "moduł" brzmi tak zajebiście) ćwiczeń z Play Framework. Pierwszy moduł pojawił się w dwóch częściach :

Poniższy materiał to krótki zbiór ćwiczeń opracowany na podstawie tutoriali i książek - przystosowany do nauki Playa w trakcie 2-3 godzinnego spotkania z przerwą na pizze czy na jakieś zdrowsze jedzenie. Program nie jest jeszcze przetestowany w życiu ale powinien płynnie pokazać uczestnikom epickość formularzy w Play gdzie testować można po prostu wszystko. Także miłej zabawy z lektury i ćwiczeń.

Gdy to tak sobie piszę jest jeszcze jedno wolne miejsca na spotkanie także jak ktoś ma ochotę niech się śmiało dopisuje -> Link do spotkania na meetupie

Co przed startem

Aby nie zamulać już na samym początku dobrze by było gdyby każdy miał już rozpakowany Play przez zipa czy activatora. Dodatkowo fajnie przypomnieć sobie podstawy w tym celu każdy musi :

  • Zrobić nową aplikację
  • Import do Eclipse czy co kto tam używa
  • Stworzyć nowy widok jako ćwiczenie - czyli kontroler,mapowanie i szablon
  • Zostawić stronę index bo tam się wyświetla link do dokumentacji
  • Jak ktoś używa eclipse to poustawiać sobie zgodnie z http://scala-ide.org/docs/tutorials/play/

Wio

Dobrze mieć pod ręką otwartą lokalną dokumentację by powklejać importy i takie tam :

import play.api.data.Forms._
import play.api.data._

Tutaj zastanawiałem się czy większą wartość edukacyjną będzie miał jakiś zabawny przykład czy też coś generycznego jak User. Trzaśniemy coś z jedzeniem to może ludzie nauczą się patrzeć na to co jedzą (no i będzie bardziej różnorodnie bo nie zerżniemy przykładu prosto z dokumentacji).

case class Meal(name: String, calories: Int)

val mealForm=Form(mealMapping)
  
lazy val mealMapping=mapping(
    "name"->text,
    "calories"->number
)(Meal.apply)(Meal.unapply)

Deklaracja mappingu na zewnątrz wydaje mi się czytelniejsza a lazy jest potrzebne bo bez tego czasami się wywala gdzieś tam wewnątrz inicjalizacja Forma.

Tak czy inaczej mając tylko deklaracje mappingu i forma można teraz pokazać na testach, iż testować można wszystko.

    "bind data to meal mapping" in {
      val data=Map("name"->"hamburger","calories"->"5000")
      val mealData=Application.mealMapping.bind(data)
      mealData.isRight must beTrue
    }
    
    "bind data to meal mapping with errors" in {
      val data=Map("name"->"hamburger","calories"->"5000b")
      val mealData=Application.mealMapping.bind(data)
      mealData.isRight must beFalse
    }
    
    "bind data to meal form with error" in {
      val data=Map("name"->"hamburger","calories"->"5000b")
      val mealData=Application.mealForm.bind(data)
      mealData.errors must have(_.key=="calories")
    }

I ogólnie fajne i czytelne asercje są. Tutaj można zrobić szybko skok do Scali i wytłumaczyć co to je to Left i Right - będzie to też dobry wstęp do walidacji formularzy.

Either

Ten temat wywołuje u niektórych dużo zabawy za pierwszym razem bo w Javie informacja o tym, że coś może się nie udać zazwyczaj nie jest zawarta w typie. I jak mamy np. coś takiego:

def divide(a:Int,b:Int)=a/b       //> divide: (a: Int, b: Int)Int
divide(4,2)                       //> res0: Int = 2
divide(4,0)                       //> java.lang.ArithmeticException: / by zero

To przez zero dzielić nie można. I można zrobić tak :

def divide(a:Int,b:Int)={
 if(b==0) throw new IllegalArgumentException("cos tam")
 a/b
}

Ale można też :

def divide(a:Int,b:Int)=
       if(b==0) None else Some(a/b)

divide(4,2).map(_+1)              //> res0: Option[Int] = Some(3)
divide(4,0).map(_+1)              //> res1: Option[Int] = None

I tutaj ludzie, którzy robią w Javie od kilku lat maja jakaś dziwną radochę, ze typ nazywa się "Cośtam" albo "Nico". Pewnie będą jakieś pytanie "a po ch.." to w ogóle jest. Zycie pokaże co z tego wyjdzie. Tak czy inaczej mając Option chyba łatwiej będzie wytłumaczyć Either.

def divide(a:Int,b:Int)=
   if(b==0) Left(new IllegalArgumentException("cos tam"))
   else Right(a/b)           //> divide: (a: Int, b: Int)Product with Serializable with scala.util.Either[Ill
                                                  //| egalArgumentException,Int]
  divide(4,2).isRight               //> res0: Boolean = true
  divide(4,0).isRight               //> res1: Boolean = false

To będzie przykład w kontekście testów, które widzieliśmy chwilę wcześniej. A w kontekście obsługi forma, której jeszcze nie było - poniższe trzy linijki

val result=divide(4,2)
val funkcjaBiznesowa=(x:Int)=>x+1
result.fold(throw _, funkcjaBiznesowa)          //> res2: Int = 3

No i tutaj chyba każdy powinien zajażyć, że funkcjaBiznesowa nie jest sponiewierana przez wyjątki co ułatwia testowanie - z drugiej strony pewnie znowu ludzie będą śmiać się z podkreślinika throw _

No to robimy formularz

@(mealForm:Form[Meal])
<html>
<head>
<title>Formularz</title>
</head>
<body>

@import helper._

@form(action=controllers.routes.Application.submitForma){
@inputText(mealForm("name"),'id->"name")
@inputText(mealForm("calories"))
<input type="submit">
}

</body>
</html>

I zmiana w routach oraz kontrolerze :

//routes
POST   /submitForma    controllers.Application.submit()

//Application
def form = Action {
    Ok(views.html.formularz(mealForm))
  }
  
  def submitForma=TODO

Czas na obsługę POSTA - tutaj wąłśnie przydaje się wiedza z ćwiczenia "Either".

def submitForma = Action { implicit request =>
    mealForm.bindFromRequest.fold(
      badForm => BadRequest(views.html.formularz(badForm)),
      _ => Ok("mealOK")
    )

  }

Na razie bez styli wygląda to koślawo ale na prostszych przykładach łatwiej się uczyć.

Dokladniejsza walidacja

Na razie walidacja jest uboga dlatego dodamy sobie kilka parametrów aby ograniczyć to co można wprowadzić i do tego wrzucimy trzecie pole opcjonalne aby pokazać jak ładnie wszystko nam się skomponuje. Warto dodać, że wszystkie validacje są w obiekcie Forms

lazy val mealMapping = mapping(
    "name" -> nonEmptyText(3, 10),
    "calories" -> number(1000, 8000),
    "description" -> optional(text)
  )(Meal.apply)(Meal.unapply)


case class Meal(name: String, calories: Int,description:Option[String])

I w ten czas jest dobry moment aby uaktualnić testy o sprawdzenie obsługi pełnego żądania http.

//routes
POST   /submitForma    controllers.Application.submitForma()

//testy
"submit user form successfully" in new WithApplication{
     val formRequest=FakeRequest(POST,"/submitForma").withFormUrlEncodedBody("name"->"hamburger","calories"->"5000")
     val result=route(formRequest).get
     status(result) must equalTo(OK)
    }
    
    "return bad request when form data is missing" in new WithApplication{
      val formRequest=FakeRequest(POST,"/submitForma").withFormUrlEncodedBody("name"->"hamburger")
      val result=route(formRequest).get
      status(result) must equalTo(BAD_REQUEST)
    }

No i technicznie nawet nie musieliśmy odpalać formularza aby wiedzieć, że wsio działa :D. Teraz czas na jeszcze jeden patent - globalna walidacja!

//Application.scala
lazy val mealMapping = mapping(
    "name" -> nonEmptyText(3, 10),
    "calories" -> number(1000, 8000),
    "description" -> optional(text)
  )(Meal.apply)(Meal.unapply) verifying("zle dane tak ogolnie", mealData=>mealData.calories>2000 && mealData.name!="Warzywo")


//test
 "bind data to user form with global error" in {
     val data=Map("name"->"Warzywo","calories"->"5000")
     val mealData=Application.mealForm.bind(data)
     mealData.errors must have(_.message=="zle dane tak ogolnie")
    }

//html
@if(mealForm.hasGlobalErrors) {
<ul>
@for(error<-mealForm.globalErrors){
<li>@error.message</li>
}
</ul>
}

I można też verifying pojedyncze pola -

"name"->text(3, 10).verifying("can not be Warzywo", _!="Warzywo")

No i pewnie padnie pytanie jak te napisy pod polami pozmieniać. To można na szybko tak :

@inputText(mealForm("calories"),'_showConstraints->false,'_help->"moj koment")

Ale zapewne to nie wystarczy i czas przejść do definiowania własnych komunikatów.

Custom messages

I teraz jest taka opcja - odpowiednie komentarze muszą pojawić się w pliku conf/messages, którego jeszcze nie ma. Wszystkie możliwe komunikaty zaś znajdziemy tam gdzie rozpakowaliśmy archiwum playa - $PLAY_HOME/framework/src/play/src/main/resources/messages

Co by ni szukać poniżej pełna lista - za długa to ona nie jest

# Default messages

# --- Constraints
constraint.required=Required
constraint.min=Minimum value: {0}
constraint.max=Maximum value: {0}
constraint.minLength=Minimum length: {0}
constraint.maxLength=Maximum length: {0}
constraint.email=Email

# --- Formats
format.date=Date (''{0}'')
format.numeric=Numeric
format.real=Real

# --- Errors
error.invalid=Invalid value
error.invalid.java.util.Date=Invalid date value
error.required=This field is required
error.number=Numeric value expected
error.real=Real number value expected
error.real.precision=Real number value with no more than {0} digit(s) including {1} decimal(s) expected
error.min=Must be greater or equal to {0}
error.min.strict=Must be strictly greater than {0}
error.max=Must be less or equal to {0}
error.max.strict=Must be strictly less than {0}
error.minLength=Minimum length is {0}
error.maxLength=Maximum length is {0}
error.email=Valid email required
error.pattern=Must satisfy {0}

error.expected.date=Date value expected
error.expected.date.isoformat=Iso date value expected
error.expected.jodadate.format=Joda date value expected
error.expected.jsarray=Array value expected
error.expected.jsboolean=Boolean value expected
error.expected.jsnumber=Number value expected
error.expected.jsobject=Object value expected
error.expected.jsstring=String value expected
error.expected.jsnumberorjsstring=String or number expected
error.expected.keypathnode=Node value expected

error.path.empty=Empty path
error.path.missing=Missing path
error.path.result.multiple=Multiple results for the given path

Walnijmy sobie takie zestawik jak poniżej i zerknijmy co wyjdzie.

#forms
constraint.required=wymagane
format.numeric=musi być numerem

custom template

Czas by bliżej przyjrzeć temu co jest generowane przez te funkcje "@inputText" - ...i to zrobimy na zajęciach a nie tutaj. W tym miejscu po prostu poznamy patent na zmianę domyślnej struktury html jeśli nam nie pasuje.

// tworzymy template playowy z htmlem
@* myFormField Template File *@
@(elements: views.html.helper.FieldElements)

<div class="formGroupMoje">
<label for="@elements.id"></label>
<div class="formInputMoje">
@elements.input
</div>
</div> //formatter pola z wykorzystaniem szablonu object MyFormConstructor { import views.html.helper.FieldConstructor implicit val myFields = FieldConstructor(myFormField.f) } // jest implicit to sam import w stronie forma wystarczy @import helper._ @import views.MyFormConstructor._

I od razu wygląd forma się zmienił. Czy na lepsze? Każda zmiana jest na lepsze poza zmianami na gorsze.

upload pliku

To ćwiczenie jest w całości zerżnięte z książki "Play for Scala developers". Wygodnie jest zadeklarować najpierw
//Application
def fileUpload=TODO
//routes
POST   /uploadFile     controllers.Application.fileUpload()
bo wtedy kompilator nie pluje się przy używaniu backward routing.
<form action="@routes.Application.fileUpload" method="post" enctype="multipart/form-data">
<input type="file" name="image">
<input type="submit">
</form>
I tutaj ważna i ciekawa rzecz - po raz pierwszy uczestnicy kursu spotykają się z parserami requestów - co będzie tematyką trzecich albo czwartych zajęć.
  def pokazPlikForm = Action {
    Ok(html.fileForm())
  }

  def fileUpload = Action(parse.multipartFormData) { request =>
    request.body.file("image").map { file =>
      file.ref.moveTo(new File("/tmp/image"),true)
      Ok(s"retrieved file ${file.filename}")
    }.getOrElse(BadRequest("file mising."))
  }

No i to tyle

No i to tyle

Brak komentarzy:

Prześlij komentarz