strona główna

Archive for the 'Programowanie' Category

Wielkanoc

Wednesday, February 25th, 2009

Kiedy ostatnio uruchomiliście serwis z przewidywanym czasem przydatności liczonym w dekadach?

Bo ja – dzisiaj :) http://KiedyBędzieWielkanoc.pl/

Opisując według opublikowanej przez Paula Grahama listy 13 istotnych cech startupu:

1. Wybierz odpowiedni zespół

No ba. Zrobione.

2. Uruchom szybko

Zrobione. Poniżej dwóch godzin od pomysłu do momentu uruchomienia publicznie dostępnej wersji.

3. Pozwól swojej wizji ewoluować

Zrobione. Już po uruchomieniu wprowadziłem dwie poważne poprawki od pierwszych użytkowników (nawigacja i kolorystyka!).

4. Zrozum użytkowników

Zrobione. Moi użytkownicy chcą wiedzieć, kiedy będzie wielkanoc. Dostarczam im tę wiedzę.

5. Lepiej mieć małą grupę użytkowników zachwyconych, niż większą – obojętnych

Pierwsze reakcje były wyjątkowo pozytywne, wszystkim użytkownikom (100%) udało się wykorzystać serwis i dotrzeć do poszukiwanych informacji. Z drugiej strony – zachwytów nie słyszałem. Może po dodaniu komponentu społecznościowego i awatarów...

6. Zaskakuj wysokim poziomem obsługi klienta

Odpowiadam na pytania. Według moich dotychczasowych doświadczeń z różnymi firmami – to jest zaskakująco wysoki poziom.

7. Dostajesz to, co mierzysz

Mierzę przychody. Serwis jest na dobrej drodze do break-even.

8. Wydawaj niewiele

Zrobione. Wydałem 2 złote 20 groszy na domeny.

9. Doprowadź startup do etapu, w którym zarabia na utrzymanie Cię przy życiu

Lada dzień...

10. Unikaj rozpraszaczy

Zrobione. Przez dwie godziny zajmowałem się tylko tym.

11. Nie zniechęcaj się

Na razie nie jest źle, nie ugiąłem się pod ciężarem świadomości roli, jaką może odegrać ten serwis w życiu ludzi którzy na niego trafią.

12. Nie poddawaj się

Zrobione. Pierwszy release był poprzedzony dwugodzinnym marszem śmierci, przez który przeszedłem zwycięsko.

13. Umowy rzadko dochodzą do skutku

Na szczęście Google rzadko odmawia.

Bilans listy jest zdecydowanie dodatni, więc pomimo kryzysu spodziewam się wielu chętnych na inwestycje.

A Ty? Nie zwlekaj! Dowiedz się kiedy będzie wielkanoc już dziś!

Django a martwe drzewa

Friday, January 16th, 2009

Po ostatnich warsztatach dostałem pytanie o książki na temat Django. Odpowiedź może być interesująca dla większej grupy ludzi, więc wklejam ją także tutaj.

Z książkami, niestety, jest marnie. Głównie dlatego, że przed wydaniem wersji 1.0 Django mocno się zmieniało.

Po polsku nie pojawiło się chyba jeszcze nic, chociaż od dłuższego czasu Helion ma "w przygotowaniu" Django. Ćwiczenia praktyczne.

Po angielsku jest The Django Book. Zostało wydane przez Apress, ale w wersji opisującej 0.96, różnic niestety jest dużo i to w ważnych miejscach: szablony, interfejs administracyjny, obsługa formularzy. Wersja dla 1.0 właśnie powstaje (fragment już można przeczytać na stronie) i jest spora szansa, że będzie aktualna dość długo, bo głównym powodem tak dużych zmian w ostatniej chwili była chęć ustabilizowania API. Jeśli czyta to ktoś z jakiegoś wydawnictwa: tak, to jest dobry moment żeby pomyśleć nad tłumaczeniem.

Więc niestety dobrej odpowiedzi nie mam. ALE Django ma naprawdę dobrze napisaną i aktualną dokumentację (bardzo o to dbają – praktycznie każdy commit zmieniający cokolwiek widocznego dla użytkownika zawiera też odpowiednią zmianę w docach), więc na razie RTFM jest najlepszym rozwiązaniem :)

Django: powiadomienia

Saturday, October 25th, 2008

Wczoraj przyszło mi do głowy, że powiadomienia o błędach to jednocześnie jeden z bardziej przydatnych i najmniej widocznych elementów Django. To dzięki nim zdarza mi się poprawić błędy i wysłać do użytkownika informację, że jakaś strona działa już poprawnie nawet jeśli nie chciało mu się zgłaszać usterki.

Na przykład:

 
from django.shortcuts import render_to_response
 
def gather_user_data(user):
    # in reality this would be more complex
    return {'email': user.email}
 
def profile_view(request):
    data = {'user': request.user}
    data.update(gather_user_data(request.user))
    return render_to_response('profile_view.html',
                              dictionary = data)
 

Widok profile_view zawiera błąd ujawniający się tylko w niektórych sytuacjach. Jeśli trafi na niego ktoś na serwerze produkcyjnym, zobaczy standardowe 500 Internal server error, a Django automatycznie wyśle do mnie taki raport:

 
Traceback (most recent call last):
 
  File "/home/marcink/checkout/django-trunk/django/core/handlers/base.py",
  line 86, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
 
  File "/home/marcink/checkout/utils/blogowe/errtest/../errtest/errapp/views.py",
  line 8, in profile_view
    data.update(gather_user_data(request.user))
 
  File "/home/marcink/checkout/utils/blogowe/errtest/../errtest/errapp/views.py",
  line 4, in gather_user_data
    return {'email': user.email}
 
AttributeError: 'AnonymousUser' object has no attribute 'email'
 
< wsgirequest GET:< QueryDict: {}>,
POST:< querydict : {}>,
COOKIES:{'django_log_selectedLevel': '0',
 'django_log_showLocation': '1',
 'sessionid': '915922a5710cf210e2f5fae47e0f8900'},
META:{'COLORTERM': 'gnome-terminal',
 'CONTENT_LENGTH': '',
 'CONTENT_TYPE': 'text/plain',
 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-BibuFgosrz,guid=c50cabac45d33af5b6936d0049024c87',
 'DESKTOP_SESSION': 'default',
 'DISPLAY': ':0.0',
 'DJANGO_DIR': '/home/marcink/checkout/django-trunk',
 'DJANGO_SETTINGS_MODULE': 'errtest.settings',
 'FCGI_PORT': '9014',
 'GATEWAY_INTERFACE': 'CGI/1.1',
 'GDMSESSION': 'default',
 'GDM_LANG': 'en_US.UTF-8',
 'GDM_XSERVER_LOCATION': 'local',
 'GNOME_DESKTOP_SESSION_ID': 'Default',
 'GNOME_KEYRING_PID': '7904',
 'GNOME_KEYRING_SOCKET': '/tmp/keyring-glnuCS/socket',
 'GTK_RC_FILES': '/etc/gtk/gtkrc:/home/marcink/.gtkrc-1.2-gnome2',
 'HISTCONTROL': 'ignoreboth',
 'HOME': '/home/marcink',
 'HTTP_ACCEPT': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
 'HTTP_ACCEPT_CHARSET': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
 'HTTP_ACCEPT_ENCODING': 'gzip,deflate',
 'HTTP_ACCEPT_LANGUAGE': 'en-us,en;q=0.5',
 'HTTP_CACHE_CONTROL': 'max-age=0',
 'HTTP_CONNECTION': 'keep-alive',
 'HTTP_COOKIE': 'sessionid=915922a5710cf210e2f5fae47e0f8900; django_log_showLocation=1; django_log_selectedLevel=0',
 'HTTP_HOST': '127.0.0.1:5003',
 'HTTP_KEEP_ALIVE': '300',
 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080922 Ubuntu/7.10 (gutsy) Firefox/2.0.0.17',
 'JAVA_HOME': '/home/marcink/devel/jre1.6.0/',
 'LANG': 'en_US.UTF-8',
 'LD_LIBRARY_PATH': '/home/marcink/checkout/ldev_comicspot/comicspot/../other',
 'LESSCLOSE': '/usr/bin/lesspipe %s %s',
 'LESSOPEN': '| /usr/bin/lesspipe %s',
 'LOGNAME': 'marcink',
 'LS_COLORS': 'no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.flac=01;35:*.mp3=01;35:*.mpc=01;35:*.ogg=01;35:*.wav=01;35:',
 'OLDPWD': '/home/marcink',
 'OTHER_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot/../other',
 'OUTER_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot/..',
 'PATH': '/home/marcink/bin:/home/marcink/local/bin:/home/marcink/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games',
 'PATH_INFO': u'/profile/',
 'PROJECT_BASE_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot',
 'PROJECT_DIR': '/home/marcink/checkout/ldev_comicspot/comicspot/comicspot',
 'PROJECT_NAME': 'comicspot',
 'PWD': '/home/marcink/checkout/utils/blogowe/errtest',
 'PYTHONPATH': '/home/marcink/checkout/django-trunk',
 'QUERY_STRING': '',
 'REMOTE_ADDR': '127.0.0.1',
 'REMOTE_HOST': '',
 'REQUEST_METHOD': 'GET',
 'RUN_MAIN': 'true',
 'SCRIPT_NAME': u'',
 'SERVER_NAME': 'raven.loc',
 'SERVER_PORT': '5003',
 'SERVER_PROTOCOL': 'HTTP/1.1',
 'SERVER_SOFTWARE': 'WSGIServer/0.1 Python/2.5.1',
 'SESSION_MANAGER': 'local/raven:/tmp/.ICE-unix/7907',
 'SHELL': '/bin/bash',
 'SHLVL': '1',
 'SSH_AGENT_PID': '7947',
 'SSH_AUTH_SOCK': '/tmp/ssh-lBRzQZ7907/agent.7907',
 'TERM': 'xterm',
 'TZ': 'America/Chicago',
 'USER': 'marcink',
 'USERNAME': 'marcink',
 'WINDOWID': '39852701',
 'WINDOWPATH': '7',
 'XAUTHORITY': '/tmp/.gdmELMXJU',
 'XDG_DATA_DIRS': '/usr/local/share/:/usr/share/:/usr/share/gdm/',
 'XDG_SESSION_COOKIE': '90de2c2a7bf31a95a76c4900471ce700-1224887428.976645-1927173936',
 '_': '/usr/bin/python',
 'wsgi.errors': < open file '< stderr>', mode 'w' at 0xb7dc30b0>,
 'wsgi.file_wrapper': < class 'django.core.servers.basehttp.FileWrapper'>,
 'wsgi.input': < socket ._fileobject object at 0x854a72c>,
 'wsgi.multiprocess': False,
 'wsgi.multithread': True,
 'wsgi.run_once': False,
 'wsgi.url_scheme': 'http',
 'wsgi.version': (1, 0)}>
 

Po pierwsze – dowiaduję się o problemie bez żadnej akcji ze strony użytkownika. Po drugie – dostaję mnóstwo informacji o błędzie: opis wyjątku (tutaj – brak atrybutu email w AnonymousUser), dokładnie wskazane miejsce wystąpienia razem z pełnym stosem wywołań funkcji, zawartość parametrów przekazanych przez GET i POST i zawartość zmiennych środowiskowych. W praktyce zwykle wystarcza to do złożenia testu pozwalającego na powtórzenie i usunięcie błędu.

„Django - 3 lata pracy i wszystko, co mamy, to marne 1.0″

Monday, October 13th, 2008

Z okazji wydania Django 1.0 na ostatnim Bootstrapie opowiedziałem o najważniejszych zmianach, jakie zaszły w tej ramówce podczas tych trzech lat, które upłynęły od momentu otwarcia kodu. Slajdy z mojej prezentacji:

Reklama: już wkrótce odbędzie się pierwsza edycja warsztatów Django – najszybszy sposób na poznanie możliwości tego środowiska i rozpoczęcie pracy.

Django na Bootstrapie

Saturday, October 4th, 2008

Już w przyszłym tygodniu Bootstrap 8.10. Andy Budd opowie o testach użyteczności (“Guerilla Usability Testing with Silverback”), a ja ponarzekam na Django ("Django - 3 lata prac i wszystko, co mamy, to marne 1.0").

Serdecznie zapraszamy.

Skróty: render_to_response

Wednesday, October 1st, 2008

Twórcy Django są bardzo skuteczni w eliminowaniu miejsc, które wymagałyby złamania zasady DRY. Tym dziwniejszy jest wyjątek render_to_response: teoretycznie skrót, w praktyce – funkcja wymagająca codziennego wpisywania prawie identycznego kodu.

Wygląda to tak:

 
from django.shortcuts import render_to_response
from django.template import RequestContext
def some_view(request):
    return render_to_response('testapp/some_view.html',
                              context_instance = RequestContext(request),
                              dictionary = {'title': 'A very simple view',
                                            'text': 'Some text here'})
 

Parametr context_instance w praktyce zawsze wygląda tak samo. Nazwa szablonu często, chociaż nie zawsze, przypomina nazwę funkcji obsługującej dany adres. Parametr dictionary bywa na tyle rozbudowany, że najczęściej buduje się go wcześniej, żeby uniknąć bardzo długich lub sztucznie podzielonych linii. Łączny poziom powtarzalności jest bardzo wyraźny po napisaniu kilku-kilkunastu widoków, zwłaszcza jeśli część z nich zawiera więcej niż jedną ścieżkę kończącą się wywołaniem render_to_response.

A przecież to może wyglądać tak:

 
@with_template
def simple_view(request):
    return {'title': 'A very simple view',
            'text': 'Some text here'}
 

W którymś momencie ta powtarzalność zirytowała mnie na tyle, że poświęciłem trochę czasu na napisanie odpowiedniego dekoratora. Dla lenistwa.

Działa tak:

  • jeśli funkcja zwraca HttpResponse, to dekorator nic nie zmienia – przekazuje ten wynik wyżej,
  • w przeciwnym wypadku zakłada, że wynikiem jest słownik (lub coś, co słownik udaje). W takiej sytuacji przekazuje go do render_to_response, zakładając że wymagany szablon to "nazwa_aplikacji/nazwa_funkcji.html", a context_instance to standardowy RequestContext.
  • dodatkowo, funkcja może zmienić nazwę szablonu: wystarczy, że w słowniku zwróci element 'template_name'.

Przykłady:

 
# plik testapp/views.py
 
@with_template
def simple_view(request):
    return {'title': 'A very simple view',
            'text': 'Some text here'}
 
@with_template
def simple_view_with_locals(request):
    title = 'Another simple view'
    text = 'Some text here'
    return locals()
 
@with_template
def complex_view(request):
    if request.REQUEST.get('redirect_me'):
        return HttpResponseRedirect('http://google.com/')
    else:
        template_name = 'testapp/some_other_template.html'
        return locals()
 
@with_template('testapp/yet_another_template.html')
def view_with_template_override(request):
    title = 'Another simple view'
    text = 'Some text here'
    return locals()
 
@login_required
@with_template
def authenticated_view(request):
    text = 'Welcome, ' + request.user.username
    return locals()
 

Można też po prostu zwrócić locals() – słownik zawierający wszystkie zmienne lokalne. Możliwe jest też, jak w ostatnim przykładzie, użycie innych dekoratorów, na przykład login_required.

Dekorator with_template ma też dodatkową, dużą zaletę: ułatwia wprowadzenie konwencji, zgodnie z którą nazwa szablonu wynika z nazwy aplikacji i funkcji. Konwencje, ogólnie, to dobra rzecz. Ta konkretna sprawia, że oszczędza się czas który normalnie poświęca się na decyzję jak nazwać szablon, wpisanie jego nazwy oraz późniejsze przeglądanie kodu żeby dowiedzieć się, jakiego szablonu należy się spodziewać pod danym adresem.

Owszem, można – jak zauważył ktoś podczas europythonowej prezentacji – dopisać informację o nazwie szablonu do dokumentacji funkcji, ale konwencje działają jak makra lub funkcje w dokumentacji: określa się je w jednym miejscu i stosuje w wielu, bez potrzeby zaglądania do tekstu.

 
# plik testapp/urls.py
 
from django.conf.urls.defaults import *
 
urlpatterns = patterns('testapp.views',
    (r'^normal/', 'normal_view'),
    (r'^simple/', 'simple_view'),
    (r'^complex/', 'complex_view'),
    (r'^authenticated/', 'authenticated_view'),
    (r'^view_with_template_override/', 'view_with_template_override'),
)
 

I trochę testów:

 
# plik testapp/tests.py
 
class WithTemplateTestCase(TestCase):
    def test_views(self):
        response = self.client.get('/app/simple/')
        self.assertTemplateUsed(response, 'testapp/simple_view.html')
 
        response = self.client.get('/app/complex/')
        self.assertTemplateUsed(response, 'testapp/some_other_template.html')
 
        response = self.client.get('/app/complex/', data = {'redirect_me': '1'})
        self.assertEqual(response.status_code, 302)
 
        response = self.client.get('/app/view_with_template_override/')
        self.assertTemplateUsed(response, 'testapp/yet_another_template.html')
 
        from django.contrib.auth.models import User
        user, created = User.objects.get_or_create(username = 'marcink')
        user.set_password('marcink')
        user.save()
 
        self.client.login(username='marcink', password = 'marcink')
        response = self.client.get('/app/authenticated/')
        self.assertTemplateUsed(response, 'testapp/authenticated_view.html')
 

A sam dekorator wygląda tak:

 
import re
 
from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.template import RequestContext
try:
    # functools appeared in Python 2.5
    from functools import wraps, update_wrapper
except ImportError:
    # luckily for us, Django already backported it
    from django.utils.functional import wraps, update_wrapper
 
def with_template(arg):
    """
    A view decorator that handles rendering.
 
    If the view returns a HttpResponse, it is passed intact; otherwise
    the returned value is passed as dictionary to render_to_response.
 
    Usage samples:
 
    @with_template
    def view_func(request, ...):
        return ...
 
    @with_template('custom/template/name.html')
    def other_view_func(request, ...):
        return ...
    """
 
    class TheWrapper(object):
        def __init__(self, default_template_name):
            self.default_template_name = default_template_name
 
        def __call__(self, func):
            def decorated_func(request, *args, **kwargs):
                ret = func(request, *args, **kwargs)
                if isinstance(ret, HttpResponse):
                    return ret
                return render_to_response(ret.get('template_name',
                                                  self.default_template_name),
                                          context_instance=RequestContext(request),
                                          dictionary=ret)
            update_wrapper(decorated_func, func)
            return decorated_func
 
    if not callable(arg):
        return TheWrapper(arg)
    else:
        app_name = re.search('([^.]+)[.]views', arg.__module__).group(1)
        default_template_name = ''.join([app_name, '/', arg.__name__, '.html'])
        return TheWrapper(default_template_name)(arg)
 

Pełen kod można znaleźć tutaj: testowy projekt with_template

Aktualizacja 2008/10/13: zgodnie z sugestią urbana dodałem możliwość przekazania nazwy szablonu jako parametru dekoratora.

Oiola.com - otwarcie

Wednesday, August 6th, 2008

Dobra wiadomość: od poniedziałku Oiola jest dostępna dla wszystkich – konto można założyć lekko, łatwo i przyjemnie, bez konieczności kontaktowania się z nami. Więcej informacji na naszym blogu, napisał też o nas Paweł Iwaniuk w serwisie Antyweb.

Zapraszamy :)

Django na TechAuli

Friday, June 27th, 2008

Na dzisiejszej TechAuli opowiedziałem o Django – dlaczego jest fajne, szybkie i warto używać. Były trafne i ciekawe pytania z sali, być może też udało mi się namówić kilka osób do wypróbowania go w praktyce, więc prezentacja wyglądała na udaną.

Oiola.com - nowa wersja

Wednesday, June 4th, 2008

Od dzisiaj na oiola.com można zobaczyć efekty ostatnich dwóch miesięcy pracy: dużo zmian, pełen spis na Oiola-blogu.

Tym razem, pomimo szczerych chęci, więcej kodu napisałem niż znalazłem. Jedyny nowy moduł znaleziony w sieci to strip-o-gram – sensownie działający konwerter z HTML na czysty tekst, przez Oiolę wykorzystwany podczas wysyłania poczty (od teraz można dowolnie zmieniać tekst powiadomień i wysyłać listy do uczestników bezpośrednio z oiola.com).

Z ciekawostek: w tej chwili Oiola, w części serwerowej, to 5433 linie kodu, z czego 2329 to testy – ta część serwisu, która zajmuje się tylko i wyłącznie sprawdzaniem, czy reszta działa poprawnie – więc sama aplikacja to 3104 linie. Wykorzystałem biblioteki zawierające w sumie 22543 linii, nie licząc samego Django i oczywiście biblioteki standardowej Pythona, co oznacza że zdecydowaną większość kodu napisał i zdebugował już ktoś inny. I to właśnie jest podstawa szybkiego tworzenia aplikacji: platforma, dla której istnieje mnóstwo bibliotek tak wysokiej jakości :)

RuPy 2008

Wednesday, April 16th, 2008

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ć :)
  • ogólnie – zdecydowanie warto było pojechać.