Wczoraj wieczorem, okrężną drogą, wróciłem z RuPy 2008.
Podsumowanie, w stylu zapożyczonym z net.to, bo pozwala na kolejność leniwą:
konferencja była świetnie zorganizowana,
lubię Poznań. Na ulicach o 4:30 w nocy są tłumy, zdecydowanie więcej ludzi niż w Krakowie o tej samej porze. O Warszawie można w tym kontekście nawet nie wspominać.
Hotel Ilon należy omijać – stan pokoi jest zupełnie inny niż to, co widać na zdjęciach na stronie WWW. Bait and switch, proszę Państwa.
kodowanie na żywo to pomyłka. Niezależnie od tego, jak bardzo zwięzły jest wybrany język, prezentacja pokazująca proces tworzenia oprogramowania co do kliknięcia i skutnięcia w klawisz ma bardzo niską gęstość informacyjną. Jeśli na dodatek prezenter nie zadba o dorzucenie co kilka minut czegoś, co ludzi rozbawi, zmartwi albo zaskoczy, publiczność odlatuje.
z prezentacji, które udało mi się obejrzeć:
ciekawe: Business Natural Languages (Jay Fields), Correlations and Conclusions (Zed Shaw) i Revolver One (pythonowy arkusz kalkulacyjny, Michael Foord z ThoughtWorks) zaprezentowany jako lightning talk,
wymagają jeszcze trochę pracy: TDD in Rails, Let the Python crawl,
blipcast był zdecydowanie mniej aktywny niż podczas Auli,
bardzo dawał się we znaki niedobór gniazdek elektrycznych. To naprawdę powinno być uregulowane odpowiednią ustawą
Mood Club and Cafe (miejsce Geek Party) – bardzo fajne, chociaż może trochę przyciasne jak na tak liczną grupę,
ciekawe różnice w sposobie prowadzenia samochodu: większość kierowców w Poznaniu i okolicach jeździ zgodnie z ograniczeniami prędkości, za to wielu wymusza pierwszeństwo i wyprzedza na ciągłej linii. Teoretycznie połączenie jest logiczne – przy założeniu, że ten z głównej jedzie z prędkością 50km/h, łatwiej jest wyliczyć jak mu się wepchnąć dokładnie przed zderzak tak, żeby nie musiał specjalnie zwalniać
Tym razem krótko: uruchomiliśmy z Jankiem osobną stronę na potrzeby comiesięcznych projektów: Binary Life Forms. Wpisy o kolejnych projektach będą pojawiać się i tutaj (w wersji bardziej technicznej) i tam (bardziej ogólne, w dwóch wersjach językowych).
Wczoraj wieczorem dokręciliśmy ostatnie śrubki w drugim z cyklu
projektów-w-tydzień. Tym razem z lekkim poślizgiem, w planach
mieliśmy zamknięcie prac w zeszłym tygodniu, w praktyce –
rzeczywiście intensywne prace zaczęliśmy w piątek, skończyliśmy
wczoraj (środa). Pierwszy
użytkownik powoli kończy przeprowadzkę.
Nowy serwis to WebComicsPot,
narzędzie do możliwie prostego publikowania w sieci komiksów, także
wielojęzycznych. Założenia były takie: samo umieszczenie plansz w
sieci jest proste i tanie, kłopotem jest za to złożenie sensownego
interfejsu pozwalającego na ich przeglądanie i przełączanie się między
tłumaczeniami. WebComicsPot rozwiązuje właśnie ten kłopot.
Jak w favpico, obsługa jest
uproszczona do granic możliwości: autor podaje tylko listę adresów pod
którymi znajdują się kolejne plansze. Serwis zakłada, że każdy adres
zawiera datę w formacie RRRR-MM-DD. Dodatkowo możliwe jest wklejenie
w stronę komiksu swojego kodu HTML i dodanie arkusza styli CSS.
Jednocześnie, to jest nasz pierwszy serwis wspierający OpenId – koniec z
wymyślaniem kolejnych haseł
Coś o stronie technicznej i organizacyjnej; zaletą tak małych
projektów jest to, że można w krótkim czasie sprawdzić w praktyce
sporo nowych narzędzi: jeśli się sprawdzą to świetnie, jeśli nie – to
upośledzają tylko jeden drobny projekt, nie ma sensu go przepisywać.
Tym razem znalazłem sporo przydatnych rzeczy.
Django-authopenid
zachowuje się bardzo sensownie, wymagała tylko drobnych poprawek
(które muszę jeszcze spakować w jeden sensowny diff i wysłać).
Instant Django to
świetny sposób na udostępnienie serwisu nieprogramiście
(np. grafikowi) pod Windows – zawiera, w jednej paczce, 2.5, django, sqlite, dość sensowny edytor tekstu i
parę skryptów które odpowiednio konfigurują środowisko. Wystarczy
dodać skrypt uruchamiający serwer (z syncdb przed startem), nauczyć
grafika korzystać z TortoiseSVN
i już, właśnie załatwiliśmy stronę techniczną współpracy.
FeedParser i FeedJack – świetne biblioteki do
obsługi RSS, używałem
ich już wcześniej w Planemoo. Tym razem FeedParser przydał się też do
czyszczenia kodu HTML wpisywanego przez autora komiksu: ze względów
bezpieczeństwa serwis usuwa część konstrukcji.
Cssutils
– podobnie jak w przypadku HTML, możliwość wpisania
dowolnego kodu CSS to dziura bezpieczeństwa ze względu na @import i
javascript w adresach. Sam cssutils nie potrafi tego wyczyścić, ale
wystarczyło utworzyć własną podklasę CSSSerializer, żeby usunąć i
@import, i podejrzane adresy:
URL_WITH_CALL_RE = re.compile(r'^url[(]["\']https?://\S+$')class SanitizingSerializer(cssutils.CSSSerializer):
"""
Overrides some stuff in CSSSerializer to make it safer.
1. disallow ALL URLs not starting with 'http://' or 'https://'
2. remove @imports
"""def do_css_CSSValue(self, cssvalue):
if cssvalue andisinstance(cssvalue, CSSPrimitiveValue):
if cssvalue.primitiveType == CSSPrimitiveValue.CSS_URI:
ifnot URL_WITH_CALL_RE.match(cssvalue._value):
return u'url("")';
returnsuper(SanitizingSerializer, self).do_css_CSSValue(cssvalue)def do_CSSImportRule(self, rule):
return u''
cssutils.setSerializer(SanitizingSerializer())
Django zawiera bibliotekę do komentarzy, bardzo przydatną ale z
paroma ograniczeniami; przeszkadzał mi, przede wszystkim, ścisły
rozdział komentarzy użytkowników zalogowanych i anonimowych, w
praktyce uniemożliwiający dyskusję między jednymi a drugimi. Tutaj
poszukiwania innej biblioteki niestety nie dało rezultatu, trzeba było
dostosować django.contrib.comments.
Drobiazg, ale jak przydatny: slughifi zamienia "zażółć gęślą jaźń"
na "zazolc-gesla-jazn", więc jest wersją funkcji slugify, mądrzejszą o
znajomość wielu znaków narodowych, w tym cyrylicy.
Musiałem też nieco rozbudować własną bibliotekę do obsługi tłumaczeń, django-multilingual;
eksperymentalne zmiany okazały się bardzo przydatne więc niedługo znajdą
się też w wersji publicznej.
Napisałem też trochę kodu wspomagającego testy modułowe Django,
teraz duża część moich testów wygląda tak:
def test_adding_a_comic(self):
self.login()self.get('/')self.click('m-your-account')self.click('publish-now')self.click('cancel')self.assertLocation('/account/ed/')# okay, now go and publish itself.click('publish-now')self.assertContains(self.response, 'name="short_name"')self.assertContains(self.response, 'name="main_language_id"')self.assertContains(self.response, 'name="add" value="save"')
Gdzie 'm-your-account' i 'publish-now' to identyfikatory (nie tekst!)
odsyłaczy na stronie; oczywiście każdy click przechodzi do strony
wskazywanej przez dany odsyłacz. Pozwala to łatwo przetestować całe
sekwencje zdarzeń, i to w sposób niezależny od aktualnie włączonej
wersji językowej interfejsu.
A za miesiąc, wszystko na to wskazuje, coś bardziej skomplikowanego
To był interesujący eksperyment: jak już napisał Janek na blogu Planemoo, postanowiliśmy stworzyć skończony, zamknięty i dopracowany serwis WWW w ciągu tygodnia (czasu rzeczywistego, a nie liczonego w godzinach-poświęconych-tylko-na-ten-projekt). Efektem jest Favpico, a przykład działania widać we wklejce po prawej stronie mojego bloga.
Serwis pozwala na tworzenie stron z listami odsyłaczy, na przykład stron startowych albo (ahem) listy znajomych. Nie jest to, oczywiście, pierwszy ani ostatni tego typu serwis, ale ten jest najprawdopodobniej najprostszy – każda strona to lista odsyłaczy reprezentowanych przez ich favicony, tak jak tu.
Przy tak niewielkiej ilości czasu konieczne było ciągłe cięcie funkcjonalności. Czy potrzebna jest zmiana koloru tła? Jasne, ale nie ma na to czasu. Usuwanie stron? To samo. Opisy przy odsyłaczach? To już barok. Odrobina składni pozwalająca na łatwiejsze tworzenie odsyłaczy do innych stron wewnątrz favpico? Barok, barok, barok. Co by tu jeszcze usunąć?
Efekt jest fajny: kompletny, zamknięty, przydatny.
Przy okazji, jeśli czyta to ktoś zajmujący się stronami Bootstrapa albo Auli Polskiej to mam prośbę: dodajcie do swoich stron favikonki. Psujecie mi stronę startową
...ale do popełniania tysięcy błędów na minutę potrzeba już komputera.
Dzisiaj o 10:03 mój błąd w kodzie El Monito spowodował wysłanie do większości użytkowników całkowicie niepotrzebnych powiadomień typu "Zmiana statusu: Online -> Online", na dodatek – do niektórych nawet w kilkudziesięciu kopiach. Dokładniej: 3211 powiadomień do 81 użytkowników, najbardziej pechowy dostał 161 (cześć, Grzegorz!). Niedobrze.
Przepraszam za kłopoty wszystkich, którzy otrzymali powiadomienia. Przyczynę już znam, jeszcze dzisiaj wprowadzę zmiany zabezpieczające przed powtórzeniem się takiej sytuacji.
Ten wpis jest wynikiem rozmów na temat możliwej rozbieżności między tym, co widać w przeglądarce, a stanem raportowanym przez Monitor.
Możliwe są, oczywiście, dwa przypadki: serwis zachowuje się poprawnie w przeglądarce, a Monitor pokazuje jego stan jako "Offline", lub na odwrót. Jeśli taka rozbieżność się utrzymuje, to należy ją potraktować jako bardzo cenną informację o trudnym do zauważenia błędzie: najprawdopodobniej serwis przesyła treści z niewłaściwym kodem HTTP.
Kody HTTP
Krótki opis, z pominięciem wielu nieistotnych teraz szczegółów: wymiana danych według protokołu HTTP odbywa się zawsze według wzoru "zapytanie-odpowiedź". Przeglądarka wysyła do serwera zapytanie zawierające adres zasobu (strony) i żądanie, co należy z nim zrobić (zwykle "pobrać" lub "zmienić"), a serwer odsyła odpowiedź z informacją o stanie wykonania polecenia oraz tekstem do wyświetlenia. Ta informacja o stanie jest przekazywana w postaci liczby – kodu odpowiedzi HTTP. Najczęściej spotykane wartości to:
200: OK – "wszystko w porządku, polecenie się powiodło, przesyłam tekst strony",
301: Trwale przeniesiony – "tej strony już tu nie ma, ale przesyłam adres pod którym powinna teraz być, spróbuj tam",
404: Nie znaleziono – "nie mam niczego z takim adresem, przesyłam wyjaśnienie",
500: Wewnętrzny błąd serwera – "wiem o co chodzi, próbowałem wykonać polecenie, ale nie wyszło; przesyłam wyjaśnienie".
Specyfikacja HTTP wymienia więcej kodów, dla wygody pogrupowanych według pierwszej cyfry: kody 2xx to różne warianty odpowiedzi "OK, jest dobrze", 3xx to "przekierowanie, spróbuj inny adres", 4xx to "błąd po stronie klienta, np niewłaściwy adres lub źle sformułowane zapytanie", 5xx to "błąd po stronie serwera, np. problem ze skryptem, bazą danych, lub sprzętem."
Co z tego wynika w przypadku opisanych wcześniej rozbieżności? Po kolei:
"Mój serwis działa, a według Monitora jest w stanie Offline"
Najprawdopodobniej Twój serwis odsyła poprawną zawartość stron, ale z niewłaściwym kodem odpowiedzi, na przykład 404 lub 500 zamiast 200. Łatwo to sprawdzić w Monitorze na stronie "historia" takiego serwisu poprzez wskazanie kursorem jednego z prostokątów z informacją o wynikach obserwacji:
To jest fragment zapisu obserwacji adresu http://elksoft.pl/nie-ma-takiej-strony. Każda stacja obserwacyjna przesyła, oprócz samego stanu (Online/Offline), także wyjaśnienie błędu, w tym przypadku – 404.
Przeglądarki zwykle nie pokazują kodów odpowiedzi, więc z punktu widzenia użytkownika wszystko może wyglądać dobrze – takie strony działają dokładnie tak samo jak z poprawnym kodem, działają odsyłacze, obrazki itp. Pomimo tego z punktu widzenia oprogramowania taki serwis nie działa. A to jest poważny problem z co najmniej dwóch powodów.
Po pierwsze, jeśli Google przeglądając zawartość serwisu trafi na takie strony, to je pominie, wychodząc z założenia że skoro serwis nie działa to cały ten tekst, który dostaje w odpowiedzi, zawiera tylko opis błędu, więc nie warto go indeksować.
Po drugie, przeglądarki i serwery proxy nie zapisują tekstów i obrazków otrzymanych z kodami oznaczającymi sytuacje błędne. Oznacza to wielokrotne ściąganie tych samych elementów stron, więc znacznie większe obciążenie serwera i połączenia z internetem.
"Mój serwis nie działa, a według Monitora jest Online"
Najprawdopodobniej mamy sytuację dokładnie odwrotną do poprzedniej. Na przykład: skrypt obsługujący serwis nie jest w stanie nawiązać połączenia z bazą danych i wyświetla opis błędu na stronie, ale przesyła odpowiedź z kodem 200, więc "jest OK".
Przeciwnie niż w poprzednim przypadku, Google i inne wyszukiwarki, polegając na kodzie odpowiedzi, potraktują ten opis błędu jak zamierzoną zawartość strony. W efekcie Twój serwis może zostać przez nie zapamiętany z niewłaściwą zawartością, aż do następnego pełnego przejrzenia zawartości.
Przeglądarki i serwery proxy, także na podstawie kodu, zapamiętają tekst z opisem błędu zamiast zawartości strony lub obrazka, co oznacza że poprawna treść może być dla części użytkowników niedostępna przez pewien czas po naprawieniu błędu.
Co z tym zrobić?
Przede wszystkim – sprawdzić. Najprościej przy pomocy programu wget (dostępne są wersje na wszystkie popularne systemy operacyjne):
Parametr –server-response oznacza, że interesuje mnie nagłówek odpowiedzi. Zawartość nagłówka można tu rozpoznać po tym, że jest wcięta. Najbardziej interesująca jest pierwsza linia: "HTTP/1.1 404 Not Found" oznacza, że odpowiedź została przesłana według protokołu HTTP w wersji 1.1, kod odpowiedzi to 404, a krótki opis: "Not Found", czyli "nie znaleziono". Tak wygląda poprawna odpowiedź serwera na nierozpoznany adres.
To wygląda ciekawiej, bo po wysłaniu zapytania o adres "http://www.elksoft.pl/" wget otrzymał odpowiedź z kodem 301 ("Moved Permanently" – "przeniesione na stałe") i adresem "http://elksoft.pl/". Prawidłową reakcją na 301 jest wysłanie kolejnego zapytania, ze wskazanym przez serwer adresem, i dokładnie to wget zrobił. Po drugim zapytaniu otrzymał odpowiedź ze statusem 200 ("OK").
Ogólnie, rezultat testów powinien być taki: kiedy w przeglądarce Twój serwis wygląda i zachowuje się poprawnie, to wywołania wget z tymi samymi adresami powinny kończyć się informacjami o statusie 2xx. Kiedy przeciwnie, serwis wyświetla informacje o błędach, wget powinien otrzymać właściwy kod z serii 4xx lub 5xx. Takie zachowanie w dużym stopniu wspierają wszelkie serwery WWW i serwery aplikacyjne – w razie wykrycia błędów, wyjątków lub innego zatrzymania skryptu w niewłaściwy sposób ustawiają kod odpowiedzi na 500. Zwykle wystarcza im nie przeszkadzać, a jeśli już skrypt przechwytuje wyjątki lub obsługuje po swojemu sytuacje błędne, to trzeba je obsłużyć do końca, włącznie z odesłaniem odpowiedniego kodu.
W sobotę odbyło się pierwsze spotkanie pod nazwą bootstrap.pl: Django, tożsamość cyfrowa i lekko licząc 4 godziny dyskusji. Na streemo.pl można obejrzeć nagrania z prezentacji, zaczęły się też pojawiać pierwsze zdjęcia.
Zapowiada się interesujący cykl spotkań.
Z nieco innej beczki, prace nad django-multilingual trwają, właśnie powstaje pierwszy wykorzystujący ją serwis internetowy. Jak zawsze w takich sytuacjach wyszło na jaw trochę nieprzewidzianych sytuacji i pomysłów na ulepszenia, ale ogólnie biblioteka zdaje egzamin
Cool stuff: Google provides free project hosting for open source software. I created a new project, django-multilingual and uploaded the prototype code there.
From now on you can use svn to get the source code and track changes. The initial version in svn is the code I posted in my previous blog entry with renamed directories, but without any other changes.
I am currently developing a website with lots of multilingual content, the most common use case being a model that contains a number of fields independent of user language and a set of translatable fields. I wanted something easy to use, fitting well with the rest of Django and hopefully elegant I ended up with half-baked multilingualization library for Django. Let me know if any of this looks useful to you.
The good news is that the library is already working; you even get to edit the translations in the admin interface. The bad news is it is not ready for production use yet. I would be happy to hear any suggestions to improve it.
Let's start with an example:
File: test/models.py
[python]
from django.db import models
from django.contrib.auth.models import User
from elksoft.multilingual.models import *
class Category(models.Model):
"""
Test model for multilingual content.
"""
# First, some fields that do not need translations
creator = models.ForeignKey(User)
created = models.DateTimeField(auto_now_add=True)
parent = models.ForeignKey('self', blank=True, null=True)
# And now the translatable fields
class Translation:
"""
The definition of translation model.
The multilingual machinery will automatically add these to the
Category class:
* get_name(language_id=None)
* set_name(value, language_id=None)
* get_description(language_id=None)
* set_description(value, language_id=None)
* name and description properties using the methods above
"""
name = models.CharField(blank=True, null=False, maxlength=250)
description = models.TextField(blank=True, null=False)
def __str__(self):
# note that you can use name and description fields as usual
return self.name
class Admin:
# again, field names just work
list_display = ('name', 'description')
class Meta:
verbose_name_plural = 'categories'
[/python]
The Translation class defines a set of translatable fields. The library extends the Category model with a number of methods and properties that give you access to and control of the translated fields; in particular you get 'name' and 'description' properties that assume you want to get or set values in the default language.
Internally the library creates a CategoryTranslation model that contains all the translatable fields. All the get_name/set_name methods work on the data in that model.
And now, the plumbing:
File: elksoft/multilingual/models.py
[python]
"""
Multilingual model support.
"""
from django.db import models
from django.db.models.base import ModelBase
from django.dispatch.dispatcher import connect
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import signals
# This is ugly, ideally languages should be taken from the DB or
# settings file. Oh well, it is a prototype anyway.
# It is important that the language identifiers are consecutive
# numbers starting with 1.
LANGUAGES = [['en', 'English'], # id=1
['pl', 'Polish']] # id=2
LANGUAGE_CNT = len(LANGUAGES)
from django.contrib.admin.templatetags.admin_modify import StackedBoundRelatedObject
class TransBoundRelatedObject(StackedBoundRelatedObject):
"""
This class changes the template for translation objects.
"""
def template_name(self):
return "admin/edit_inline_translations.html"
def get_default_language():
# you might take the ID from elsewhere, ie
# cookies or threadlocals
return 1
def finish_multilingual_class(*args, **kwargs):
"""
Create a model with translations of a multilingual class.
"""
class TransMeta:
ordering = ('language',)
trans_name = main_cls.__name__ + name
# create get_'field name'(language) and set_'field
# name'(language) methods for all the translation fields.
# Add the 'field name' properties while you're at it, too.
for fname, field in cls.__dict__.items():
if isinstance(field, models.fields.Field):
getter = getter_generator(trans_name, fname)
setattr(main_cls, 'get_' + fname, getter)
# delay the creation of the *Translation until the master model is
# fully created
connect(finish_multilingual_class, signal=signals.class_prepared,
sender=main_cls, weak=False)
# modify ModelBase.__new__ so that it understands how to handle the
# 'Translation' inner class
_old_new = ModelBase.__new__
def multilingual_modelbase_new(cls, name, bases, attrs):
if 'Translation' in attrs:
attrs['Translation'].contribute_to_class = classmethod(translation_contribute_to_class)
return _old_new(cls, name, bases, attrs)
setattr(ModelBase, '__new__', staticmethod(multilingual_modelbase_new))
[/python]
There are at least two ugly parts: the LANGUAGES array and overriding ModelBase.__new__. The former lets me use lots of standard admin machinery while the latter makes the library as simple to use as possible. The multilingual app needs to appear in INSTALLED_APPS before any app with translatable models.
On the other hand, it really shows the nice thing about Django: you can do a lot without changing its source code.
This template tweaks the admin interface for inline models to hide language identifiers. As a result, you get something like the image to the right instead of standard inline editor.
[python]
from django import template
from django import forms
from django.template import Node, NodeList, Template, Context, resolve_variable
from django.template.loader import get_template, render_to_string
from django.conf import settings
import math
import StringIO
import tokenize
register = template.Library()
from elksoft.multilingual.models import LANGUAGES
def language_code(language_id):
"""
Return the code of the language with id=language_id
"""
return LANGUAGES[language_id - 1][0]
register.filter(language_code)
def language_name(language_id):
"""
Return the name of the language with id=language_id
"""
return LANGUAGES[language_id - 1][1]
register.filter(language_name)
[/python]
And that's it, the whole library for now.
As for the bad news I mentioned earlier, this is what I believe needs to be done before the library gets really useful:
caching: right now every access to get_name or set_name accesses the database; ideally the library should retrieve all translations for a single Category with a single SELECT and save them only with Category.save,
sorting: name and description for the default language should be appended as extras to every queryset with Category.objects,
specifying the default language in querysets and model instances: Category.objects.all().for_language('pl')[0].name should return the value for Polish language,
another "backend", one that uses name_en, name_pl, name_de fields in the Category model instead of creating a separate model; this would speed everyting up at the expense of more complicated database updates when you change the list of available languages. The API would not change.
You can get the complete source code here. Let me know what you think.
Update: the newest version of this code is now available at Google Code as django-multilingual. There were multiple improvements and some API changes, so if you want to try it make sure you get the code from there.
2006-07-05 13:53 - Priority changed to '1 - Highest'
2006-07-11 11:34 - Task status changed to 'Active'
2006-07-26 19:36 - Task status changed to 'Closed'
2006-07-26 19:36 - Resolution set to 'Fixed'
Narzekałem tutaj i tutaj. Starym programistycznym zwyczajem dokumentacja jest jeszcze w powijakach, ale wygląda na to, że funkcjonalność już jest.