# Maják

Maják je CMS pro Pirátské weby. Postavený je na [Wagtail](https://wagtail.io/).

[![code style: Black](https://img.shields.io/badge/code%20style-Black-000000)](https://github.com/psf/black)
[![powered by: Wagtail](https://img.shields.io/badge/powered%20by-Wagtail-43b1b0)](https://wagtail.io)
[![powered by: Django](https://img.shields.io/badge/powered%20by-Django-0C4B33)](https://www.djangoproject.com)
[![licence AGPLv3](https://img.shields.io/badge/licence-AGPLv3-brightgreen)](LICENSE)

## Pod pokličkou

[Wagtail](https://wagtail.io) a [Django](https://www.djangoproject.com) jsou poměrně
vyspělé frameworky. Vždy mysli na to, že problém co řešíš, už pravděpodobně řešil
někdo před tebou. A obvykle existuje elegantní řešení.

Pár užitečných odkazů:

* [docs Wagtail](https://docs.wagtail.io/)
* [docs Django](https://docs.djangoproject.com/)

A za zmínku stojí [Awesome Wagtail](https://github.com/springload/awesome-wagtail)
jako přehled pluginů a rozšíření pro Wagtail.

Rozšíření která používáme:

* [wagtail-metadata](https://github.com/neon-jungle/wagtail-metadata)
  - upravený template `shared/templates/wagtailmetadata/parts/tags.html`

### Struktura projektu

    .
    ├── donate          = app na web dary.pirati.cz
    ├── elections2021   = app na količní web k volbám 2021
    ├── senate          = app na web senat.pirati.cz
    ├── senat_campaign  = app na weby kandidátů na senátory
    ├── districts       = app na web kraje.pirati.cz
    ├── district        = app na weby oblastních sdružení
    ├── uniweb          = app na univerzalni webove stranky
    ...
    ├── majak           = Django projekt s konfigurací Majáku
    ├── shared          = app se sdílenými static soubory a templaty pro weby
    ├── calendar_utils  = app s modelem a utilitami na iCal kalendáře
    ├── tuning          = app na tuning administračního rozhraní Majáku
    └── users           = app s custom user modelem a SSO, apod.

Poznámky k uspořádání:

* nesdílíme a nemícháme modely zděděné z `Page` mezi jednotlivými appkami a weby,
  abychom předešli rozbití cizího webu
* části definice tříd stránek sdílíme jako mixin pattern (`shared.models.ArticleMixin`,
  `share.models.SubpageMixin`, apod.)
* sdílet se mohou obyčejné Django modely z `models.Model`, ale umístěné v nějaké
  appce/knihovně určené pro sdílení (`shared`, `calendar_utils`, apod.), nikoliv
  mezi weby (mezi appkami na weby by nikdy neměly vzniknout závislosti)

### Konfigurace webu

Konfigurace konkrétního webu (odkazy do patičky, Matomo ID, apod.) se definuje v
kořenové `xxxHomePage` webu. Je to pro uživatele snažší na správu než
[Site Settings](https://docs.wagtail.io/en/stable/reference/contrib/settings.html)
Wagtailu pro konfigurace webů. A pro vývojáře je to skoro jedno.

Z různých podstránek webu se k té konfiguraci dostaneme přes property `root_page`
kterou přidává `shared.models.SubpageMixin`. V `xxxHomePage` webu je třeba
definovat `root_page` tak, aby vedla na `self`.

### Styleguide

Některé weby využívají [Pirátskou UI styleguide](https://gitlab.pirati.cz/to/weby/ui-styleguide)
pro vzhled. Idea je, že se budou sdílet nejen statické assety (CSS, JS, ...),
ale i univerzální kusy template, které se dají includovat v různých webech
(patička webu, karta článku do přehledu, atp.).

Sdílené části využívané na více webech se umísťují do:

    shared/static/styleguideXX/     = statické assety (CSS, JS, obrázky, ...)
    shared/templates/styleguideXX/  = snippety pro include v templatech

`XX` v názvu adresáře je číslo major a minor verze šablony. Např. `styleguide18`
obsahuje věci ze šablony verze `1.8.x`. Důvodem je snažší migrace na novější
verze šablony, které mohou obsahovat nekompatibilní změny. Každý web tak může
(a nemusí) migrovat nezávisle dle potřeby.

Různé verze šablony jsou k vidění na [styleguide.pirati.cz](https://styleguide.pirati.cz/)

### Kalendáře

Pro práci s kalendáři v iCal formátu je připravena appka `calendar_utils`.

Poskytuje `CalendarMixin` do modelu, který přidá fieldy `calendar_url` pro
editaci a `calendar` pro vazbu na model `Calendar` (který se plní a automaticky
spravuje na pozadí). Typicky se použije ve Wagtail settings pro web, kde stačí
`calendar_url` zpřístupnit pro editaci.

Kalendář se stáhne při uložení modelu obsahujícího `CalendarMixin`.

Appka přidává management command `update_callendars`, který stahuje a updatuje
kalendáře. Je třeba ho pravidelně volat na pozadí (přes CRON).

### Celery (Import z Jekyllu)

Import z Jekyll GitHub repozitářů pirátských webů je řešen asynchroně přes Celery.
Celery využívá Redis, který může běžet např. lokálně, typicky 6379:
V envu pak je pak potřeba mít nastavený Celery broker:

```
CELERY_BROKER_URL=redis://localhost:6379/6
CELERY_RESULT_BACKEND=redis://localhost:6379/6
```

Aby se celery tasky vykonávaly, je potřeba pustit celery v terminálu:

```
celery -A majak worker
```

Pokud není zadán `CELERY_BROKER_URL`, tak se automaticky nastaví
`CELERY_TASK_ALWAYS_EAGER = True`, čímž se tasky vykonají synchronně přímo v
procesu webserveru.

### Stránka 404

Pokud je třeba vlastní 404 pro web, stačí do kořenové `xxxHomePage` webu
definovat metodu `get_404_response` vracející Django HTTP Reponse objekt.

    def get_404_response(self, request):
        return HttpResponse("Stránka nenalezena", status=404)

## Deployment

### Konfigurace

Je třeba nastavit environment proměnné:

| proměnná | default | popis |
| --- | --- | --- |
| `DATABASE_URL` | | DSN k databázi (např. `postgres://user:pass@localhost:5432/majak`) |
| `OIDC_RP_REALM_URL` | | OpenID server realm URL (např. `http://localhost:8080/auth/realms/master/`) |
| `OIDC_RP_CLIENT_ID` | | OpenID Client ID |
| `OIDC_RP_CLIENT_SECRET` | | OpenID Client Secret |
| `BASE_URL` | https://majak.pirati.cz | základní URL pro notifikační emaily apod. |

V produkci musí být navíc nastaveno:

| proměnná | default | popis |
| --- | --- | --- |
| `DJANGO_SECRET_KEY` | | tajný šifrovací klíč |
| `DJANGO_ALLOWED_HOSTS` | | allowed hosts (více hodnot odděleno čárkami) |
| `CELERY_BROKER_URL` | | URL pro Celery Broker |
| `CELERY_RESULT_BACKEND` | | URL pro Celery Result Backend |
| `EMAIL_HOST` | SMTP pro odesílání přihlášek do kariér |
| `EMAIL_HOST_USER` | --||-- Username |
| `EMAIL_HOST_PASSWORD` | --||-- Heslo |
| `EMAIL_PORT` | --||-- Port |
| `CLAMD_TCP_ADDR` | ClamAV host (pro skenování virů v nahraných souborech) |
| `CLAMD_TCP_SOCKET` | ClamAV socket |

Různé:

| proměnná | default | popis |
| --- | --- | --- |
| `MAJAK_ENV` | prod | `prod`/`test`/`dev` prostředí kde Maják běží |
| `SENTRY_DSN` | | pokud je zadáno, pády se reportují do Sentry |
| `SEARCH_CONFIG` | english | nastavení jazyka fulltextového vyhledávání, viz níže |
| `DEBUG_TOOLBAR` | False | zobrazit Django Debug Toolbar (pro vývoj) |
| `MAILTRAIN_API_URL` | | URL k API Mailtrain |
| `MAILTRAIN_API_TOKEN` | | token k API Mailtrain |

Settings pro appky na weby:

| proměnná | default | popis |
| --- | --- | --- |
| `DONATE_PORTAL_REDIRECT_URL` | "" | URL pro přesměrování z darovacího formuláře |
| `DONATE_PORTAL_REDIRECT_SOURCE` | dary.pirati.cz | identifikátor zdroje pro přesměrování na darovací portál |
| `DONATE_PORTAL_API_URL` | "" | URL s API darovacího portálu |

### Management commands

Přes CRON je třeba na pozadí spouštět Django `manage.py` commandy:

* `clearsessions` - maže expirované sessions (denně až týdně)
* `publish_scheduled_pages` - publikuje naplánované stránky (každou hodinu)
* `update_callendars` - stáhne a aktualizuje kalendáře (několikrát denně)
* `update_main_timeline_articles` - aktualizuje články na `pirati.cz` z `https://piratipracuji.cz/api/`
* `update_redmine_issues` - aktualizuje programované body MS a KS stránek napojených na Redmine (několikrát denně)

### Fulltextové vyhledávání v češtině

Pro fulltextové vyhledávání je třeba do PostgreSQL přidat
[slovníky](https://github.com/f00b4r/postgresql-czech) do adresáře
`/usr/local/share/postgresql/tsearch_data/`.

V databázi Majáku je třeba nakonfigurovat český fulltext:

```sql
CREATE TEXT SEARCH DICTIONARY cspell (template=ispell, dictfile=czech, afffile=czech, stopwords=czech);
CREATE TEXT SEARCH CONFIGURATION czech (copy=english);
ALTER TEXT SEARCH CONFIGURATION czech ALTER MAPPING FOR word, asciiword WITH cspell, simple;
CREATE extension IF NOT EXISTS unaccent;
```

Otestovat funkčnost lze dotazem:

```sql
SELECT * FROM ts_debug('czech', 'Příliš žluťoučký kůň se napil žluté vody');
```

Dále nastavit environment proměnnou `SEARCH_CONFIG=czech`. A nakonec jednorázově
spustit naplnění indexu pro vyhledávání `python manage.py update_index`.
Aktualizace indexu poté probíhají automaticky při uložení stránek.

### Přidání nového webu

Doména či subdoména se musí nakonfigurovat v:

* environment proměnné `DJANGO_ALLOWED_HOSTS`
* proxy před Majákem

## Vývoj

Pro vývoj je definován pomocný `Makefile` pro časté akce. Pro nápovědu zavolej:

    $ make help

### Lokální instalace a spuštění

#### Vytvoření virtualenv pro instalaci závislostí

Vytvoř virtualenv:

    $ make venv

Vytvoří virtualenv ve složce `.venv`. Předpokládá že výchozí `python` v terminálu
je Python 3. Pokud tomu tak není, použijte třeba [Pyenv](https://github.com/pyenv/pyenv)
pro instalaci více verzí Pythonu bez rizika rozbití systému.

#### Aktivace virtualenvu

Před prací na projektu je třeba aktivovat virtualenv. To bohužel nejde dělat
pomocí nástroje `make`. Je třeba zavolat příkaz:

    $ source .venv/bin/activate

Můžete asi na to vytvořit alias pro shell. Do `~/.bash_profile` nebo `~/.zshrc`
nebo jiného konfiguračního souboru dle vašeho shellu přidejte:

    alias senv='source .venv/bin/activate'

A pak můžete virtualenv aktivovat pouze jednoduchým voláním:

    $ senv

Pro sofistikovanější řešení, které vám aktivuje virtualenv při změně adresáře na
adresář s projektem, slouží nástroj [direnv](https://direnv.net/).

Deaktivace virtualenvu se dělá příkazem:

    $ deactivate

#### Instalace závislostí

V aktivovaném virtualenvu spusťte:

    $ make install

To nainstaluje Pythonní závislosti pro vývoj projektu na lokále.

#### Nastavení environment proměnných

Environment proměnné (viz konfigurace výše) se načítají ze souboru `.env`, který
může vypadat takto:

    DATABASE_URL=postgres://db:db@localhost:5432/majak
    OIDC_RP_REALM_URL=http://localhost:8080/auth/realms/master/
    OIDC_RP_CLIENT_ID=majak
    OIDC_RP_CLIENT_SECRET=abcd

Pro lokální vývoj obsahují settings tyto výchozí hodnoty:

    DEBUG = True
    ALLOWED_HOSTS = ["*"]
    MAJAK_ENV = "dev"

#### Autentifikace a autorizace

Jako OIDC server můžete použít lokálně spuštěný Keycloak z [Dev Tools](https://gitlab.pirati.cz/to/dev-tools).

Prvním přihlášením přes OIDC se vytvoří uživatel, ale bez oprávnění. Je třeba
mu přes Django shell (viz níže) nastavit oprávnění pro přístup do administrace:

```python
from users.models import User
u = User.objects.get()
u.is_superuser = True
u.is_staff = True
u.save()
```

### Management projektu

#### Migrace databáze

Aplikuj migrace databáze:

    $ make migrate

Při změně modelů vygeneruj migrace pomocí:

    $ make migrations

#### Spuštění development serveru

Django development server na portu `8006` se spustí příkazem:

    $ make run

Poté můžete otevřít web na adrese [http://localhost:8006](http://localhost:8006)

##### Debug Toolbar

Pro spuštění development serveru s Django Debug Toolbar nastavte environment
proměnnou `DEBUG_TOOLBAR`. Např.:

    $ DEBUG_TOOLBAR=1 make run

#### Django shell

Django shell používající `shell_plus` z Django extensions spustíte:

    $ make shell

### Lokální weby

Pro vývoj různých webů si nastavte v `/etc/hosts` (na Linux, Mac OS, apod.)
pojmenované aliasy k lokální IP 127.0.0.1 jako např.:

    127.0.0.1   majak.loc dalsiweb.loc

K Majáku spuštěnému v development serveru na portu `8006` se pak dostanete na
adrese [http://majak.loc:8006/admin/](http://majak.loc:8006/admin/).

A pokud nějakému webu v nastavení v Majáku dáte adresu třeba `dalsiweb.loc` a
port `8006`, dostanete se pak na něj přes adresu
[http://dalsiweb.loc:8006](http://dalsiweb.loc:8006).

### Testy

Používá se testovací framework [pytest](https://pytest.org). Spuštění testů:

    $ pytest

Případně přes `make`, ale bez možnosti parametrizovat spuštění testů:

    $ make test

Coverage report:

    $ make coverage

### Code quality

K formátování kódu se používá [black](https://github.com/psf/black). Doporučujeme
ho nainstalovat do vašeho editoru kódu, aby soubory přeformátoval po uložení.

Přeformátování kódu nástrojem `black` je součástí `pre-commit` hooks (viz níže).

Součástí `pre-commit` hooků je také automatické seřazení importů v Pythonních
souborech nástrojem [isort](https://github.com/timothycrosley/isort/).

### Pre-commit hooky

Použivá se [pre-commit](https://pre-commit.com/) framework pro management git
pre-commit hooks.

Máte-li pre-commit framework [nainstalovaný](https://pre-commit.com/#installation)
spusttě příkaz:

    $ make install-hooks

Ten naisntaluje hooky pro projekt. A poté při každém commitu dojde k požadovaným
akcím na změněných souborech.

Ručně se dají hooky na všechny soubory spustit příkazem:

    $ make hooks

## Upgrade závislostí

K upgrade se používají [pip-tools](https://github.com/jazzband/pip-tools) (`pip install pip-tools`):

    $ cd requirements/
    $ pip-compile -U base.in
    $ pip-compile -U production.in
    $ pip-compile -U dev.in

Tím se vygenerují `base.txt`, `production.txt` a `dev.txt`.