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">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ęć.
<input type="file" name="image">
<input type="submit">
</form>
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