Użytkowników w bazie powiedzmy, że już mam. Teraz wypadałoby zrobić coś, aby dać im możliwość zarejestrowania się w serwisie.
Proces ten jest prosty jak konstrukcja gwoździa. Wpisujemy login, wpisujemy hasło (dwa razy), wpisujemy napisik z CAPTCHA i po walidacji formularza ( np. nick unikalny i hasło nie za krótkie ) tworzone jest konto w serwisie. Do tego można się na nie nawet zalogować! Zasadniczo skoro domenę już mamy (przynajmniej na początek), widok sobie zaraz stworzymy, kontroler też ogarniemy. Jedyną rzeczą, o której póki co nie za bardzo mam pojęcie to wdrożenie CAPTCHY. Sensowność jej wdrożenia polega na tym, iż jest to plugin do frameworka, a takowymi jeszcze się nie bawiłem (no chyba że skryptami/pluginami do wrzucenia aplikacji na serwer mor.ph).
Dla procesu rejestracji oraz późniejszego logowania stworzyłem w poprzednim wpisie kontroler Users. Teraz należy usunąć z niego wpis do scaffoldingu i zabrać się do samodzielnej pracy. Tworzymy akcję register, a także stosowny widok. Kod kontrolera jest oczywisty, widok (registry.gsp) to też prosty formularz. Wkleiłbym kod (używam tam znaczników GSP), ale niestety WordPress wygrał ze mną i za cholery nie dało się mimo wpisania encji, etc. umieścić tego kodu tutaj w ładnej formie. Chyba czas przesiąść się na Joggera czy coś.
Pluginy do Grailsów możemy znaleźć na stronie domowej projektu. Ja postanowiłem użyć JCAPTCHA, polecanego w książce “Beginning Groovy and Grails”. Co prawda w treści wyżej wymienionej pozycji używa się prostszego rozwiązania, nie ma sensu przepisywać kodu, lepiej czegoś nowego się nauczyć. JCAPTCHA jest oficjalnym rozszerzeniem frameworka zatem wystarczy w konsoli wpisać (w katalogu naszej aplikacji):
grails install-plugin jcaptcha
By po chwili mielenia cieszyć się zaimportowanym rozszerzeniem. Tutaj dopiero zaczyna się przygoda. JCAPTCHA to nie w kij dmuchał i z nim trzeba już się trochę nagimnastykować. Wpierw trzeba wyedytować plik konfiguracyjny:
jcaptchas
{
image = new GenericManageableCaptchaService(
new GenericCaptchaEngine(
new GimpyFactory(
new RandomWordGenerator("abcdefghijklmnopqrstuvwxyz1234567890"),
new ComposedWordToImage(new RandomFontGenerator(
20, 30, [new Font("Arial", 0, 10)] as Font[]),
new GradientBackgroundGenerator(140, 35,
new SingleColorGenerator(Color.white),
new SingleColorGenerator(new Color(152, 245, 255))),
new NonLinearTextPaster(6, 6, new Color(108, 123, 139))
)
)
),
180,
180000)
}
Nie pytajcie co i z czym. Gotowca ściągnąłem z Disco Bloga. Dla mnie wystarczy, gdybym zaczął kombinować, aby tło było różowe, a literki czarne i do tego robiło BLING BLING co 2 sekundy to może bym się wgłębiał w meandry tego zapisu. Nie muszę to i nie zawracam sobie głowy. Na samym początku pliku należy dodać importy odpowiednich plików – lista pod ww. adresem. Dodać też trzeba linijkę w kodzie widoku, aby wrzucił nasz anty-spam do formularza:
<input type="text" id="captcha" name="captcha" value=""/>
<jcaptcha:jpeg name="image"/>
Oto zdjęcie z efektem mojej pracy:

Pozostaje tylko napisać kod kontrolera i logikę. Logika jest prosta – walidacja przekazanych danych i lecimy do przodu. Najistotniejszy wydaje się być kontroler, a także mechanizm do pokazywania błędów walidacyjnych (wrzucę to do layoutu by działał uniwersalnie wszędzie). Wymienione powyżej funkcjonalności głupio byłoby powielać w co drugim kontrolerze – choć i to jest metoda, przynajmniej w przypadku walidacji. Jednakże tutaj będę mądry i stworzymy takie coś jak service, czyli usługę. Jest to po prostu oddzielna klasa, która oferuje pewną funkcjonalność, a którą możemy łatwo wywołać z poziomu kontrolera. Co więcej – usługi są singletonami (domyślnie) z całym dobrodziejstwem inwentarza.
Usługę utworzyć prosto – prawyklik na katalogu Services w NetBeans, wpisujemy nazwę klasy (u mnie formValidation) i mamy gotową usługę. Mocą konwencji do nazwy klasy jest dopisywany suffix ‘Service’. Oto wygenerowany kod:
class FormValidateService {boolean transactional = truedef serviceMethod() {}
}
W miejsce serviceMethod rzecz jasna trzeba wcisnąć nazwę metody, a także jej ciało. Kiedy tak się stanie mamy gotową na każde wezwanie usługę. Ja postanowiłem stworzyć dwie takowe – jedną uniwersalną do walidacji pól formularza, a także jeszcze jedną do translacji nazw pól (pole ‘name’ w elementach formularza) na polskie odpowiedniki celem ich ładnego wyświetlenia przy ew. błędzie walidacji. Oto kod usługi formValidate:
class FormValidateService {static int FIELD_INCORRECT = 0;
static int FIELD_COMPARE_INCORRECT = 1;
String fieldNamePl;
String fieldConfirmNamePl;
String defaultMessage;
boolean transactional = true
def translateFieldServiceHashMap validateForm( ArrayList Data ) {
HashMap dataValidated = new HashMap();
Boolean isValidated = true;
for( HashMap mapParams : Data ) {
if( mapParams.getAt('paramGiven') == mapParams.getAt('paramExpected') ) {
// dataValidated.putAt( mapParams.get('index'), true );
} else {
fieldNamePl = translateFieldService.translateName( mapParams.get('index') );
fieldConfirmNamePl = translateFieldService.translateName( mapParams.get('index_confirm') );
int compareStatus = mapParams.getAt('compareStatus');
if( compareStatus == FIELD_INCORRECT ) {
defaultMessage = 'Pole [' + fieldNamePl + '] ma niepoprawną wartość!';
}
else if( compareStatus == FIELD_COMPARE_INCORRECT ) {
defaultMessage = 'Pole [' + fieldNamePl + '] ma wartość inną niż pole [' + fieldConfirmNamePl + ']';
}
else {
defaultMessage = 'Pole [' + fieldNamePl + '] ma niepoprawną wartość!';
}
dataValidated.putAt( mapParams.get('index'), new org.springframework.validation.FieldError( 'FormValidateService', fieldNamePl, fieldNamePl, true, new String[1], new Object[1], defaultMessage ) );
isValidated = false;
}
}
return [ validated: isValidated , data: dataValidated ];
}
}
Jak już wspomniałem usługi to singletony (jest to zachowanie domyślne, ale jak wszystko w Grails daje się to zmienić), a odwoływanie się do nich jest banalne. Wystarczy w kontrolerze Users wpisać taki kod:
def formValidateService
I od tej pory do każdej z metod tejże usługi możemy odwoływać się z dowolnego miejsca w kontrolerze. Jak widać na powyższym przykładzie z poziomu kodu usługi możemy odwoływać się do funkcjonalności innej usługi. Oto jeszcze kod małej usługi tłumaczącej – można to oczywiście oprzeć o domyślne ustawienia i plikach z domyślnymi komunikatami. Póki co działa to w takiej formie, gdyż wówczas mamy dowolność – trzymania komunikatów np. w bazie danych, niekoniecznie zaś na plikach (co ułatwia często pracę). Kod:
class TranslateFieldService {boolean transactional = trueString translateName( String toTranslate ) {
String returnString = '';
HashMap translations = new HashMap();
translations.put( 'login', 'login' );
translations.put( 'passwd', 'hasło' );
translations.put( 'passwd_confirm', 'potwierdzenie hasła' );
returnString = ( translations.containsKey( toTranslate ) ) ? translations.getAt( toTranslate ) : toTranslate ;
return returnString;
}
}
I tyle nasze usługi. Powiedzmy, że dają one pewną funkcjonalność, choć z całą pewnością można by je ulepszyć. Dla naszej aplikacji ważne będzie też miejsce, gdzie możnaby wyświetlić informacje przeznaczone dla uzytkownika (błędy walidacji, informacje o charakterze neutralnym). By to osiągnąć dodałem trochę magii w pliku layoutu main.gsp, ale nie jest to nic wykraczającego poza zwykły IF. No i teraz nadszedł czas na kontroler:
class UsersController {def defaultAction = "index"
def formValidateService
def JcaptchaService
def index = { }
def register = {
if( session.user == null ) {
HashMap validationResults;
if( request.method == "POST" ) {
validationResults = formValidateService.validateForm(
[ [index: 'passwd', index_confirm: 'passwd_confirm',paramGiven: params.passwd.toLowerCase(),paramExpected: params.passwd_confirm.toLowerCase(), compareStatus: formValidateService.FIELD_COMPARE_INCORRECT ],
[index: 'captcha', paramGiven: JcaptchaService.validateResponse( 'image', session.id, params.captcha), paramExpected: true ] ] )
if( validationResults.getAt('validated') == true ) {
Htj_Users user = new Htj_Users( nick: params.nick, passwd: params.passwd.encodeAsMD5(), role: Htj_User_Role.get(1) );
if( user.save() ) {
session.user = user
flash.message = 'Rejestracja zakończona pomyślnie'
redirect(uri: '/')
} else {
flash.errors = user.errors.getAllErrors()
}
} else {
flash.errors = ((HashMap) validationResults.getAt('data')).values()
}
[ params: params ]
}
} else {
flash.message = 'Użytkownik już jest zalogowany!'
redirect( uri: '/' )
}
}
def login = {
if( session.user == null ) {
if( request.method == 'POST' ) {
Htj_Users user =
Htj_Users.findByNick(
params.login )
if( user != null ) {
session.user = user;
redirect( uri: '/' );
} else {
HashSet errors = new HashSet();
errors.add(new org.springframework.validation.FieldError( 'FormValidateService', 'login', 'login', true, new String[1], new Object[1], 'Podano złe dane!' ));
flash.errors = errors
}
}
} else {
flash.message = 'Już jesteś zalogowany!'
redirect( uri: '/' )
}
}
def logout = {
session.user = null;
flash.message = 'Zostałeś pomyślnie wylogowany!'
redirect( uri: '/' );
}
}
Jak przekonuję się po raz kolejny WordPress ma strasznie kijowe formatowanie kodu. Dlatego też sugeruję skopiowanie powyższego do IDE i przejrzenie kodu w bardziej przyjaznej formie. Myślę, że nie powinien on sprawić trudności. Poniżej kilka screenów z efektów działania programu:
Błąd rejestracji:

Pomyślna rejestracja i zalogowanie:

To tyle. Oczywiście w realnie działającej aplikacji tak przeprowadzony proces rejestracji wołałby o pomstę do nieba – najlepiej byłoby dodać dla użytkowników pole e-mail, aby można było przesłać na wskazany adres mejl aktywujący (no i później w aplikacji wygodniej byłoby posiadać adresy swoich użytkowników). Również usability pewnikiem kuleje, podobnie jak sam kod :) Jednakże tak jakoś głupio wyszło, że napisanie tego posta zajęło mi dwa tygodnie więc podaruję sobie te małe błędy.