pkwbackend_prez



pkwbackend_prez

1 0


pkwbackend_prez


On Github miastojestnasze / pkwbackend_prez

Wyręczamy PKW

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.
Z komisji wyborczych zaczynały napływać niepokojące informacje o chaosie, który tam panuje
Co się stało? Nawalił program do liczenia głosów. To dzięki temu, że był tak zabugowany, komisje były zmuszone cofnąć się do lat '90 i przeliczyć głosy ręcznie.
Koniec końców oficjalnie wyniki otrzymaliśmy chyba we wtorek.
Z jednym ale. Nie dla Warszawy. I tak sobie czekaliśmy na te wyniki dla Warszawy przez kilka miesięcy, aż w końcu uznaliśmy w Miasto Jest Nasze, że pora ich wyręczyć. Kolega napisał wniosek o informację publiczną.
I otrzymał pliki z tabelkami excela na podstawie których zrobiłem stronę pkw.miastojestnasze.org, którą Wam teraz pokażę a potem opowiem co się pod nią kryje.
Na tej stronie możemy wybrać typ wyborów, tutaj akurat rada dzielnicy, oprócz tego możemy uszczegółowić geografię, np. dzielnica Srodmiescie, pyk i mamy statystyki. Możemy zobaczyć na wykresie kto wygrał, możemy sprawdzić jaka frekwencja a sprawdzić czy na przykład Jan Spiewak dostał się do rady dzielnicy. I to jest tyle jeśli chodzi o stronę. Wbrew pozorom pod maską kryje się kilka ciekawych rozwiązań, o czym Wam teraz opowiem.
Powróćmy do tabelek excela, które Wam pokazałem. One są mega nieczytelne i żeby sensownie te dane opublikować, wypada zrobić aplikację internetową.

http://okfnlabs.org/dataconverters/

A pierwszym krokiem była ich konwersja z xls na json przy użyciu skądnikąd narzędzia napisanego w pythonie, czyli dataconvers.
Gdy to zrobiłem, me oczy ujrzały taki śliczny json, który nie ma wad xlsa. I tu pojawia się pokusa szybkiego wypuszczenia tych statystyk, bo to wszystko wydaje się banalne do zrobienia. I tak też myślałem, i wręcz chciałem pójść drogą bylejakiegowykonania aplikacji. Ale w międzyczasie robiłem inny projekt poboczny i zrozumiałem, że to jest dobre wyjście. Postanowiłem wybrąć o wiele trudniejszą drogę.

Python + Django + PostgreSql

+ 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'.
Najważniejsze w tej aplikacji są oczywiście dane, a żeby móc je zwizualizować, to trzeba je wrzucić. A zanim się je wrzuci, wypada zaprojektować modele djangowe. Mamy taki typowy json, który zawiera w sobie informacje o wszystkim co jest w danym obwodzie wyborczym... Problem opjawia się przy przechowywania głosów na danego kandydata lub partię w danym obwodzie wyborczym. Otóz liczba partii jest różna zależnie typu wyborów, okręgu wyborczego. I tu się tu się rodzi pytanie, jak sensownie to wrzucić do baza danych?
    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ć.

Import danych do bazy

    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.

Rozwiązanie?

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.

Frontend

  • Wieloplikowy Gulp
  • Jade
  • Stylus
  • I dużo bólu związanego ze znajdowaniem działających frontowych bibliotek
Postanowiłem uciec w stronę frontu z dwóch powodów: 1. Chciałem już mieć cos do pokazania ludziom z MJN. Aplikacja zaliczała ogromną obsuwę i nikt już w nią nie wierzył 2. Chciałem wiedzieć jakie endpointy potrzebuję i w jakim formacie dane chcę mieć.

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.
Ale w końcu to się udało, na początku listopada. Tymczasem deadline zbliżał się nieuchronnie, bo na 16. listopada była przewidziana premiera appki. To był równy rok od fakapu pkw. A aplikacja wcale nie wyglądała tak jak na tym screenie. Tam nic nie działało. Kompletnie. Ale postanowiłem jednak się zaprzeć i skończyć ją przed deadlinem, pomimo tego że mam etatową i wyjątkowo angażującą pracę.

Jak uzyskać dane z wyborów?

    
    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.

Areas Tree

{
    "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.
I spiąłem to. Ludzie byli zachwyceni, dostałem zielone światło a także ostateczny deadline - poniedziałek 16. listopada. I tak do niedzieli kończyłem te aplikacje, jednakże pojawił się pewien istotny mankament. Ona wyglądała jak kupa. W niedzielę o 23:30 premiera pkw.miastojestnasze.org została przełożona a jej strona wizualna miała zostać podrasowana przez głównego programistę w MJN.
Tymczasem następnego dnia w poniedziałek 16. listopada stało się coś dziwnego. Gazeta Wyborcza opublikowała artykuł o fakapie pkw. Musieliśmy aplikację opublikować tak szybko jak się tylko da. Wzieliśmy się ostro do roboty.
Lukasz Fiszer w ciągu jednego wieczora całkowicie odmienił stronę. Nota bene niewiele zmieniająć w samym kodzie frontowym.

Optymalizacja

  • Caching czasochłonnych obliczeń
  • Ograniczenie liczby requestów do back-endu
  • Gzip
Natomiast ja w tym czasie zająłem się jej optymalizacją, bo takie rzeczy powinno robić się na końcu. Wykonałem prostych czynności takich jak...

Rezultat?

Na środę aplikacja była gotowa. Mogliśmy startować.
Ten projekt pokazał mi warto przykładac się do tworzenia podstaw nawet w tak małych aplikacjach jak pkw, bo to potem wynagradza w trudnych chwilach. Naprawdę nie ma sensu iść na wielkie kompromisy i lepiej pisać program tak dobrze jak się potrafi, bo potem ten czas się zwraca.

Chciałbyś zrobić niesamowity projekt?

napisz na: kontakt@miastojestnasze.org

github: https://github.com/miastojestnasze

Projekty dla MJN zdecydowanie warto robić. MJN ma ogromną siłę medialną w Warszawie i jeśli coś fajnego wykonacie, będzie o Was naprawdę głośno. W dodatku nikt was nie ogranicz w doborze technologii. Robicie to jak chcecie, kiedy chcecie i uczycie się super rzeczy. Naprawdę warto do nas dołączyć.

Thx!

lordzfc@gmail.com

pytania?
Wyręczamy PKW 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.