"The Grid. A digital frontier. I tried to picture clusters of information as they move through the computer. What did they look like? Ships? Motorcycles? Were the circuits like freeways? I kept dreaming of a world I thought I’d never see. And then, one day, I got in." — Tron: Legacy

2012-12-11

Smalltalk : Seaside z użyciem Pharo, cz.3

(c) Robert Tinney

Mam już schemat jak wyciągnąć komentarze z dowolnego bloga, to teraz czas na część wizualną. Tak jak poprzednio, najpierw z grubsza opiszę poszczególne kroki. Film z podsumowaniem dam na końcu. Jako, że film byłby dość długi gdyby pokazywać tworzenie kodu to pokażę w nim tylko jak wygląda struktura pakietu i kod. Z poprzedniej części mam już w zasadzie napisaną podstawę pojedynczej funkcji, która odczyta komentarze z podanego adresu URL i zwróci tablicę komentarzy. Najpierw jednak opiszę jak utworzyć komponent w Seaside.
Otwieram System Browser i zaczynam od utworzenia kategorii (paczki obiektów). Prawy myszki na liście kategorii i klikam "Add category...". Jako, że tworzę pod portal dobreprogramy.pl, utworzę kategorię DobreprogramyAPI.

Tworzenie komponentu głównego

Ten komponent będzie odpowiadać za zawartość strony głównej. To wszystko o czym będę pisać jest także dostępne dostępne w dokumentacji Seaside.. Kasą główną komponentów jest klasa WAComponent. Po niej odziedziczę funkcjonalność na potrzeby mojej nowej klasy "DPRoot":

WAComponent subclass: #DPRoot
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'DobreprogramyAPI'

Pierwsze co trzeba zrobić, to utworzyć metodę "initialize":

initialize
   super initialize.

Metoda "initialize" jest metodą wywoływaną zaraz po utworzeniu obiektu, m.in. po wywołaniu metody "new". Dobrą praktyką jest wywołać metodę "initialize" obiektu nadrzędnego za pomocą metody "super". Zielona strzałka do góry, która się pokaże obok nazwy metody mówi, że moja metoda nadpisuje metodę z klasy nadrzędnej.
Kolejnym krokiem będzie utworzenie metody, która wyświetli coś na stronie. Na chwilę obecną dam jakiś prosty tekst byle zobaczyć, czy działa. Metoda ta posiada określona nazwę, bo Seaside przeszukując obiekty i wyświetlając zawartość szuka właśnie jej:

renderContentOn: html
   html paragraph: 'Alibaba'.

Jako, że obiekt DPRoot jest obiektem głównym, trzeba zaznaczyć to tworząc metodę, która poinformuje framework Seaside. W części klasowej trzeba utworzyć metodę:

canBeRoot
   ^ true.

Dzięki niej Seaside będzie widzieć obiekt w przeglądarce aplikacji.

Rejestracja komponentu jako aplikacji

W przeglądarce przechodzę pod adres http://localhost:8080/config . Klikam w Add na pasku menu. W pole wpisuję 'dp' i wybieram typ jako Application. OK. W sekcji General na kolejnej stronie jako 'Root class' wybieram DPRoot. Klik w Apply. Teraz przechodząc pod adres localhost:8080/dp powinien się wyświetlić Alibaba... Jest. ;)
To samo można zrobić wywołując w Workspace polecenie:

WAAdmin register: DPRoot asApplicationAt: 'dp'.


Tworzenie elementu, który wyświetli pojedynczy komentarz

Komponent tworzę tak samo jak komponent główny, z jednym wyjątkiem. Komponent będzie posiadać prywatne pola, przechowujące dane nicku, logo i treść komentarza:

WAComponent subclass: #DPKoment
    instanceVariableNames: 'nick imgUrl text'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'DobreprogramyAPI'

Teraz trzeba utworzyć gettery i settery. Najlepiej zaznaczyć obiekt DPKoment, potem prawym myszki klik, wybrać Refactor class -> Accessors i kliknąć Accept. OK. Można teraz modyfikować zmienne instancji po utworzeniu obiektu. Aby oszczędzić sobie zachodu niepotrzebnym późniejszym pisaniem trzeba jeszcze utworzyć konstruktor na postawie tych zmiennych. Tworzę go w części klasowej obiektu:

nick: aNick imgUrl: aImgUrl text: aText.
    ^ (self new) nick: aNick; imgUrl: aImgUrl; text: aText.

Czas na metodę renderContentOn:. Będzie dość prymitywna. Upiększanie będzie dalej w części z CSS. Teraz tylko oznaczę odpowiednio klasy tagów.

renderContentOn: html
    html div class: 'komentarz';
        with: [
            html div class: 'nick'; with: self nick.
            html div class: 'image'; with: [html image url: self imgUrl].
            html div class: 'text'; with: self text.
            ].

Użyłem tu kilku słów kluczowych takich jak: div, class image. Odpowiadają one odpowiednikom z HTML zgodnie z tym wykazem pod kolumną 'Factory Selector'.
Zmieniam metodę renderującą klasy DPRoot na taką, by wypróbować czy działa:

renderContentOn: html
 | tmp |

 tmp := DPKoment nick: 'test nick' imgUrl: 'http://localhost:8080/files/WAWelcomeFiles/seasidestar.png' 
                 text: 'tekst'.

 html paragraph: tmp.

Wygląda to mniej więcej tak:

Mając gotowy komponent można spróbować pobrać i wyświetlić komentarze z bloga DP. Najlepiej będzie to zrobić tworząc obiekt, który przechwyci listę przez parametr i wyświetli ją w sobie. Nazwę go 'DPKomentList'.

WAComponent subclass: #DPKomentList
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'DobreprogramyAPI'

DPkomentList będzie mieć konstruktor, który za parametr bierze listę z komentarzami

fromList: aList
    ^ (self new) comments: aList.

oraz metodę instancji i zmienną która przechowa przekazane komentarze do wyświetlenia: 'comments'. Mając te rzeczy można napisać metodę renderującą:

renderContentOn: html
    | dpk |
    (self comments) do: [ :koment |
        dpk := DPKoment nick: (koment at: #nick) imgUrl: (koment at: #img) text: (koment at: #text).
        html paragraph: dpk.
        ]

Jako, że komentarze będą aktualizowane z jednego miejsca utworzę zmienną klasową w obiekcie DPRoot o nazwie blogData. Oczywiście tworzę metody dostępu do tej zmiennej. blogData ma postać listy, której elementy to obiekty Dictionary:

pseudo kod: OrderedCollection : ( Dictionary :  #url, #name, #data).

Po stronie klasy tworzę metodę, która wyciąga z adresu url bloga komentarze. Metoda ma taki sam wygląd jak w poprzednim wpisie o Pharo:

commentsFromUrl: aUrl
| zupa root komentIDregX komentClass imgClass nickClass komentSoups komenty koment |
zupa := Soup fromUrl: aUrl.
root := zupa findTagByID: 'ctl00_phContentLeft_panUpdateComment'.

komentIDregX := 'komentarz_[0-9]+'.
komentClass := 'text-h75 tresc'.
imgClass := 'border small float-left'.
nickClass := 'text-h65 font-heading display-inl_blk nick'.

komentSoups := root findAllTagsByIDregX: komentIDregX.
komenty := OrderedCollection new.
komentSoups do: [ :ks |
    koment := Dictionary new.
    koment at: #nick put: (ks findTagByClass: nickClass) text.
    koment at: #img put: (ks findTagByClass: imgClass) src.
    koment at: #text put: (ks findTagByClass: komentClass) text.
    komenty add: koment.
    ].

^ komenty.

To jest metoda statyczna dostępna z dowolnego obiektu. Wykorzystam ją w metodzie inicjalizacyjnej DPRoot, którą rozszerzę do postaci:

initialize
   | tmp |
super initialize.
    
   (DPRoot blogData) ifNil: [ DPRoot blogData: OrderedCollection new.].

   tmp := OrderedCollection new. 
   DPRoot blogData do: [ :data |
    data at: #data put: (DPRoot commentsFromUrl: (data at: #url)).
    tmp add: data.
    ].

  DPRoot blogData: tmp.

Metoda ta uzupełnia pole #data w słowniku na liście na podstawie pola #url. Słowniki przy dodawaniu bloga do listy wyświetlania posiadają zainicjowane tylko pola #url i #name. Pole #name to czytelna nazwa bloga.
Metodę renderContentOn: z tej klasy zmienia poniższą:

renderContentOn: html

 html div
      script: (html jQuery new tabs
         selected: selectedTab;
         onSelect: (html jQuery ajax
            callbackTabs: [ :event | selectedTab:= event at: #index ]));
      with: [
         html unorderedList: [
            DPRoot blogData do: [ :blog |
               html listItem: [
                  html anchor
                     url: (html jQuery ajax
                        html: (DPKomentList fromList: (blog at: #data ));
                        fullUrl);
                     with: (blog at: #name) ]]]]

Jest to lekko przerobiona metoda z przykładu do jQuery ( http://demo.seaside.st/javascript/jquery-ui/tabswidget ). Służy ona do wyświetlania kilku wpisów jako listę z zakładkami. Jest tu dodatkowa zmienna, która służy do zapamiętywania aktualnie wybranej zakładki: selectedTab. Przy zapisie metody, gdy edytor zapyta się co z nią zrobić, zaznaczam 'declare instance'. Zanim przejdę do wyświetlania komentarzy dodam sobie dwa testowe blogi do blogData. W Workspace wpisuję to i daję "Do it":

DPRoot blogData: (OrderedCollection new).
e := Dictionary new.
f := Dictionary new.

e at: #url put: 'http://...(cut)... .html'; at: #name put: 'Od kuchni'; at: #data put: nil.
f at: #url put: 'http://...(cut)... .html'; at: #name put: 'Etui'; at: #data put: nil.

DPRoot blogData add: e; add: f.

Po przejściu na localhost:8080/dp widać to:


Zwykła lista bez jQuery. Oczywiście nic się nie wyświetli, bo skrypty jQuery nie są dodane do konfiguracji. Dodać je można wchodząc w Configure na dolnym pasku. W sekcji General - Libraries te biblioteki trzeba ustawić w takiej kolejności:


Po modyfikacji powinno być widać to:


a po kliknięciu w przycisk, to:


Na dzisiaj wystarczy. W kolejnej części będzie o tym jak dorobić formularz z możliwością edycji obserwowanych blogów.
I jeszcze film na koniec z podglądem jak to wszystko wygląda w Seaside:


4 komentarze:

  1. Wracając do dyskusji rozpoczętej w innym portalu, podaję link do ciekawego artykułu:

    http://www.chris-granger.com/2012/10/05/all-ideas-are-old-ideas/

    Przypomniał mi się jeszcze jeden edytor Acme -- napisany przez jednego z twórców języka Go.

    http://acme.cat-v.org/
    http://research.swtch.com/acme

    Używał go również Dennis Ritchie -- twórca języka C.

    Pozdrawiam

    OdpowiedzUsuń
    Odpowiedzi
    1. Edytor, który współgra z kodem jest nie do przecenienia, bo umożliwia szybkie jego testowanie. Light Table posiada Instarepl, gdzie można testować całe bloki kodu nie tylko jedną funkcję na raz. Bardzo to ułatwia pracę z zagadkami, które rozwiązuje się na stronie 4clojure.com.

      W Light Table podoba mi się także instalacja pomysłu, który porzuca edytowanie kodu na całych plikach ma rzecz nawigacji po pojedynczych funkcjach jak w Smalltalk.

      Usuń
    2. Co do testowania całych bloków kodu ostatnio odkryłem nakładkę na IDLE -- Pythonowy shell + edytor kodu -- o nazwie IdleX (http://idlex.sourceforge.net). Zastosowano tam fragmenty kodu "Subcode", oddzielone komentarzami jak na screenie:

      http://idlex.sourceforge.net/static/idlex1.png

      Pozwala to uruchomić dowolny subcode, a nawet więcej. Widoczne pod menu podstawowe operatory mogą być pomocne przy testowaniu dowolnego fragmentu kodu. Wystarczy postawić kursor w miejscu gdzie znajduje się zmienna, która ma być np. zwiększana o 1 lub mnożona przez 1.1 i klikać wybrany operator, a wskazany fragment kodu będzie kolejno wykonywany.

      Testowanie bloków kodu jest też od dawna w IPython (http://ipython.org/). Z resztą nie tylko testowanie. Jest tam wiele innych magicznych funkcji jak %history, %save, %pastebin, które mogą działać na fragmentach kodu, który został wprowadzony, np.

      In [8]: %pastebin 6-7
      Out[8]: 'https://gist.github.com/3119227'

      Krótka prezentacja:

      https://docs.google.com/a/tynecki.pl/viewer?url=https://speakerd.s3.amazonaws.com/presentations/50055cd882ba61000204cacf/IPython.pdf

      Ciekawostka: IPython właśnie dostał $1.15M dofinansowania, więc można się spodziewać, że to jeszcze bardziej przyspieszy rozwój.

      Usuń
    3. Faktycznie, ten IPython to bardzo ciekawa bestia. Ci ludzie są na dobrej drodze do Smalltalka, tylko jeszcze trochę tam za dużo pisania w konsoli. Musze się temu przyjrzeć trochę bliżej.

      Ogólnie w Pythonie przydałby się browser obiektów taki jak w Smalltalk. Wtedy łatwiej byłoby się poruszać po obiektach i je modyfikować. Tego w Python brakuje. Bardzo utrudnia to jego naukę i wymaga nauki wielu standardowych funkcji na pamięć lub ciągłej pracy z dokumentacją. W Smalltalku po prostu mogę sobie podejrzeć jak coś działa i z czego się bierze bezpośrednio. Z tego co widzę, w IP jest jakiś trik, który próbuje wyciągnąć kod źródłowy funkcji, ale coś dużo przy tym pisania. Nie lubię języków, które zmuszają ludzi do klepania, bo robią z mich maszynistów, jednak dobrze widzieć krok w dobrym kierunku.

      Usuń