pkw.miastojestnasze.org
W tej prezentacji opowiem Wam o tym jak wyręczyłem PKW... Bo jeśli pamiętacie, półtora roku temu wybory samorządowe nie do końca się udały. Już pod sam koniec dnia było wiadomo, że nie doczekamy się wyników wyborów zbyt szybko.+ dwa osobne repozytoria na backend i frontend
Czyli zbudowania aplikacji po bożemu. Zacząłem zbudowania najważniejszej części aplikacji czyli backendu. I do tego celu używałem tej trójcy świętej: python bo jest fajny, django bo ma niski próg wejścia (a zaznaczę że nie jestem backendowcem), postgres bo wydawał się najlepszym rozwiązaniem do realizacji tego zadania. Nie ukrywam że wybrałem ją po dyskusjach z CTO z mojej firmy. + ważna informacja - dwa repozytoria. Jedno na front, drugie na backend. Typowy pattern, prawilnie o nim przypominam, gdy zmniejsza bałagan w aplikacji. I skoro już mamy przygotowany stack, to pora się zabrać za kodowanie. Put your speaker notes here. You can see them pressing 's'.class Election(models.Model): votes = models.CharField(max_length=2047, default=None) # "[{'political_party': 'PO', 'amount':2137}]"Mój pierwszy polegał na tym, aby trzymać (eee makarena) przekonwertowaną do stringa tablicę obiektów z hashami takimi jak... To już samo w sobie brzmi źle. I to była bardzo zła koncepcja, gdyż de facto skazujemy się tym sposobem na wiele niepotrzebnych obliczeń, które nam potem spowolnią aplikację. I co najważniejsze, tym sposobem nie wykorzystujemy największej siły postgresa.
class Vote(models.Model): election = models.ForeignKey(Election, null=True, blank=True) political_party = models.CharField(max_length=2047, default=None) amount = models.IntegerField(default=0)Czyli tego, że jest świetną relacyjną bazą danych. I po konsultacji z CTO z mojej firmy stworzyłem całkowicie nowy model, który to ma. Dzięki temu można na nim stosować wszystkie fajne operacje typu sumowanie i grubowanie. To okazało się niezwykle pomocne przy pisaniu endpointów. Ok,już mamy abstrakcje, to wypada dane wrzucić.
def create_models(request): json_file = json.load(request.FILES['file']) type = request.POST['options'] for obj in json_file: new_model = {'election_type': type} votes = [] for k, v in obj.iteritems(): if 'Pkt' in k: try: new_model['notes'].append({k: v}) except: new_model['notes'] = [{k: v}] continue k_coded = coder[k.encode('utf-8')] if 'kw' in k_coded or 'prez' in k_coded: vote = Vote(political_party=k, amount=v) vote.save() votes.append(vote) else: if 'Gmina' == k: new_model['district'] = v else: new_model[coder[k.encode('utf-8')]] = v try: new_model['notes'] = json.dumps(new_model['notes']) except: pass if type != 'candidate': election_model = Election(**new_model) election_model.save() elif type == 'candidate': election_model = Candidate(**new_model) election_model.save() continue for v in votes: v.election = election_model v.save()I zrobiłem to źle. Ale od początku. Napisałem widok do wrzucania jsonów, gdzie pod spodem była taka brzydka funkcja. Ona nie musiała szybko działać, gdyż takie operacje wykonuje się relatywnie rzadko. I tak też było - ona w najgorszym wypadku wrzucała dane 3 minuty.
Co poszło nie tak?
Coś co nie było problemem na serwerze lokalnym, okazało się nim na heroku, które po 30 sekundach braku odpowiedzi ze strony aplikacji pokazuje Ci faka. O tym problemie dowiedziałem się dzień przed premierą aplikacji, dlatego nie miałem czasu faktycznie rozwiązać problem. Pokornie przekopiowałem bazkę z lokalnej instancji serwera na heroku.Background Taski
Aczkolwiek po tym wszystkim dowiedziałem się, że takie coś najlepiej rozwiazać używając background tasków. Ok, mamy więc dane wrzucone do aplikacji, i co można robić? Endpointy? Nie, frontend.Angular.js + Nvd3.js + Smart Tables
Tylko pojawił się tu jeden problem: nie wiedziałem ta appka będzie wyglądała. Ja nie jestem specjalistą od UX, w zasadzie go nie umiem. Dlatego postanowiłem zaprojektowac solidna stack frontowy, który będzie łatwo prototypować. I zajęło mi to koszmarnie dużon czasu, bo frontend od 15 lat cierpi na chorobę wieku dziecęgo. Bardzo trudno tu znaleźć działającą bibliotekę, która nie gryzie się z resztą toolkita.urlpatterns = [ url(r'^api/stats/geography/$', get_geography, name='geography'), url(r'^api/stats/(?P.+)/district-(?P[\w|\W]+)/circle-(?P[0-9]+)/circuit-(?P[0-9]+)/$', get_stats, name='electoral-circuit-stats'), url(r'^api/stats/(?P.+)/district-(?P[\w|\W]+)/circuit-(?P[0-9]+)/$', get_stats, name='district-stats'), url(r'^api/stats/(?P.+)/district-(?P[\w|\W]+)/circle-(?P[0-9]+)/$', get_stats, name='electoral-circle-stats'), url(r'^api/stats/(?P.+)/circle-(?P[0-9]+)/$', get_stats, name='district-stats'), url(r'^api/stats/(?P.+)/circle-(?P[0-9]+)/circuit-(?P[0-9]+)/$', get_stats, name='district-stats'), url(r'^api/stats/(?P.+)/district-(?P[\w|\W]+)/$', get_stats, name='district-stats'), ]Został właściwie tydzień skończenia w miarę działającej wersji aplikacji. Tego dnia miałem zaprezentować pkw przed ludźmi z MJN. Ten drugi z deadline'ów decydował o tym, czy w ogóle MJN odważy się firmować tę stronę swoim logo a przy okazji da ogromne wsparcie medialne. I w tej sytuacji rodzi się ogromna pokusa, aby skończyć jak najszybciej i byle jak. Ale ja nadal postanowiłem jednak zrobić inaczej. We wspomnianym tygodniu zrobiłem przede wszystkim główną część backendu endpointy dla danych z wyborów. Jak sami widzicie tych endpointów jest trochę. Na samym początku przeraziłem się że przyjdzie mi pisać osobny widok zależnie od urla. Na szczęście przypomniałem sobie, że python wręcz zachęca do zrobienia tego lepiej. Spójrzcie na zmienne w urlach, np. district.
class Address(models.Model): number_of_district = models.IntegerField(default=0) district = models.CharField(max_length=50, default=None, null=True, blank=True) number_of_electoral_circuit = models.IntegerField(default=0) number_electoral_circuits = models.IntegerField(default=0, null=True, blank=True) class Meta: abstract = True class Election(Address): election_type = models.CharField(max_length=511, default=None) notes = models.TextField(default='[]')Tą samą nazwę mamy w abstrakcji ORM'em. Co można z tym fantem zrobić?
def get_stats(request, **kwargs): votes_kwargs = {} for k, v in kwargs.iteritems(): if k != 'political_party': votes_kwargs['election__' + k] = float(v) if 'number' in k else v for v in Vote.objects.filter(**votes_kwargs).distinct('political_party'): votes_kwargs['political_party'] = v.political_party obj = { 'political_party': v.political_party, 'amount': Vote.objects.filter(**votes_kwargs).aggregate(Sum('amount'))['amount__sum'] } votes_kwargs.pop('political_party') obj['percentage'] = round(float(obj['amount'])/ Vote.objects.filter(**votes_kwargs).aggregate(Sum('amount'))['amount__sum']*100, 2) district_data['votes'].append(obj) return JsonResponse(district_data)Użyć do filtrowania. A przy okazji zamiast pisać siedem widoków, użyć kwargów do tego. I dzięki wystarczy tylko jeden.
{ "name": "Wybory do Rady Miasta", "type": "city_council", "children": [], "url": "stats/city_council/" }Mamy więc jsony z danymi, więc pora nawigację na witrynie, która ułatwi nam proszenie o konkretny typ danych. Dlatego zrobiłem endpoint z drzewkiem regionów, który składa się z takich node'ów jak np. ten. Dzięki temu frontend nie musi tego robić po swojej stronie. To bardzo przyśpiesza działanie aplikacji. I ok, mamy endpointy, jest niedziela dwa dni przed prezentacją dla MJN a na froncie NIC nie działa. Kompletnie nic. Co w takiej sytuacji się robi? ZAPIERDALA. Po prostu, w tej sytuacji się zapierdala, aby otworzyć oczy tym niedowiarkom.
napisz na: kontakt@miastojestnasze.org
github: https://github.com/miastojestnasze
lordzfc@gmail.com