diff --git a/.env b/.env index e544b5b3..25754a90 100644 --- a/.env +++ b/.env @@ -1,4 +1,42 @@ ENVIRONMENT=development DJANGO_SECRET_KEY=secret_key -ALLOWED_HOSTS=* + +DEV_SOCKET_PORT=9003 + +POSTGRES_DB=project +POSTGRES_USER=user +POSTGRES_PASSWORD=insecure + +DATABASE_NAME=project +DATABASE_USER=user +DATABASE_PASSWORD=insecure +DATABASE_HOST_OVERRIDE=database +DATABASE_PORT_OVERRIDE=5432 + +DJANGO_SUPERUSER_USERNAME=admin +DJANGO_SUPERUSER_EMAIL=admin@admin.com +DJANGO_SUPERUSER_PASSWORD=insecure + +MELDINGEN_URL=http://core.mor.local:8002 +MELDINGEN_TOKEN_API=http://core.mor.local:8002/api-token-auth/ +MELDINGEN_USERNAME=regie_username +MELDINGEN_PASSWORD=insecure + +DJANGO_USER_CORE_USERNAME=core_username +DJANGO_USER_CORE_PASSWORD=insecure + +OIDC_RP_CLIENT_ID=mor-regie +OIDC_RP_CLIENT_SECRET=insecure +OIDC_OP_AUTHORIZATION_ENDPOINT=http://127.0.0.1:5556/auth +OIDC_OP_TOKEN_ENDPOINT=http://dex:5556/token +OIDC_OP_USER_ENDPOINT=http://dex:5556/userinfo +OIDC_OP_JWKS_ENDPOINT=http://dex:5556/keys + + +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +DATABASE_USERNAME=fusionauth +DATABASE_PASSWORD=hkaLBM3RVnyYeYeqE3WI1w2e4Avpy0Wd5O3s3 +ES_JAVA_OPTS="-Xms512m -Xmx512m" +FUSIONAUTH_APP_MEMORY=512M diff --git a/.env.test b/.env.test index e544b5b3..01e10192 100644 --- a/.env.test +++ b/.env.test @@ -2,3 +2,24 @@ ENVIRONMENT=development DJANGO_SECRET_KEY=secret_key ALLOWED_HOSTS=* + +POSTGRES_DB=project +POSTGRES_USER=user +POSTGRES_PASSWORD=insecure + +DJANGO_SUPERUSER_USERNAME=admin +DJANGO_SUPERUSER_EMAIL=admin@admin.com +DJANGO_SUPERUSER_PASSWORD=insecure + +DATABASE_NAME=project +DATABASE_USER=user +DATABASE_PASSWORD=insecure +DATABASE_HOST_OVERRIDE=database +DATABASE_PORT_OVERRIDE=5432 + +OIDC_RP_CLIENT_ID=mor-regie +OIDC_RP_CLIENT_SECRET=insecure +OIDC_OP_AUTHORIZATION_ENDPOINT=http://127.0.0.1:5556/auth +OIDC_OP_TOKEN_ENDPOINT=http://dex:5556/token +OIDC_OP_USER_ENDPOINT=http://dex:5556/userinfo +OIDC_OP_JWKS_ENDPOINT=http://dex:5556/keys diff --git a/.github/workflows/build-test-docker-images.yaml b/.github/workflows/build-test-docker-images.yaml index 99fd3bf9..43a6420b 100644 --- a/.github/workflows/build-test-docker-images.yaml +++ b/.github/workflows/build-test-docker-images.yaml @@ -20,7 +20,7 @@ jobs: - name: Build Docker images run: docker compose -f docker-compose.test.yaml build - name: Create Docker network - run: docker network create mor_network + run: docker network create regie_network - name: Start images run: docker compose -f docker-compose.test.yaml up -d - name: Run Tests diff --git a/.gitignore b/.gitignore index 4e505f53..8c424a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ yarn-error.log *.py[cod] *.pytest_cache* **/webpack-stats.json +**.bash_history +media/**/* +app/*.log diff --git a/LICENCE.MD b/LICENCE.MD new file mode 100644 index 00000000..ce0eff17 --- /dev/null +++ b/LICENCE.MD @@ -0,0 +1,97 @@ +OPENBARE LICENTIE VAN DE EUROPESE UNIE v. 1.2. +EUPL © Europese Unie 2007, 2016 +Deze openbare licentie van de Europese Unie („EUPL”) is van toepassing op het werk (zoals hieronder gedefinieerd) dat onder de voorwaarden van deze licentie wordt verstrekt. Elk gebruik van het werk dat niet door deze licentie is toegestaan, is verboden (voor zover dit gebruik valt onder een recht van de houder van het auteursrecht op het werk). Het werk wordt verstrekt onder de voorwaarden van deze licentie wanneer de licentiegever (zoals hieronder gedefinieerd), direct volgend op de kennisgeving inzake het auteursrecht op het werk, de volgende kennisgeving opneemt: + In licentie gegeven krachtens de EUPL +of op een andere wijze zijn bereidheid te kennen heeft gegeven krachtens de EUPL in licentie te geven. + +1.Definities +In deze licentie wordt verstaan onder: +— „de licentie”:de onderhavige licentie; +— „het oorspronkelijke werk”:het werk dat of de software die door de licentiegever krachtens deze licentie wordt verspreid of medegedeeld, en dat/die beschikbaar is als broncode en, in voorkomend geval, ook als uitvoerbare code; +— „bewerkingen”:de werken of software die de licentiehouder kan creëren op grond van het oorspronkelijke werk of wijzigingen ervan. In deze licentie wordt niet gedefinieerd welke mate van wijziging of afhankelijkheid van het oorspronkelijke werk vereist is om een werk als een bewerking te kunnen aanmerken; dat wordt bepaald conform het auteursrecht dat van toepassing is in de in artikel 15 bedoelde staat; +— „het werk”:het oorspronkelijke werk of de bewerkingen ervan; +— „de broncode”:de voor mensen leesbare vorm van het werk, die het gemakkelijkste door mensen kan worden bestudeerd en gewijzigd; +— „de uitvoerbare code”:elke code die over het algemeen is gecompileerd en is bedoeld om door een computer als een programma te worden uitgevoerd; +— „de licentiegever”:de natuurlijke of rechtspersoon die het werk krachtens de licentie verspreidt of mededeelt; +— „bewerker(s)”:elke natuurlijke of rechtspersoon die het werk krachtens de licentie wijzigt of op een andere wijze bijdraagt tot de totstandkoming van een bewerking; +— „de licentiehouder” of „u”:elke natuurlijke of rechtspersoon die het werk onder de voorwaarden van de licentie gebruikt; — „verspreiding” of „mededeling”:het verkopen, geven, uitlenen, verhuren, verspreiden, mededelen, doorgeven, of op een andere wijze online of offline beschikbaar stellen van kopieën van het werk of het verlenen van toegang tot de essentiële functies ervan ten behoeve van andere natuurlijke of rechtspersonen. + +2.Draagwijdte van de uit hoofde van de licentie verleende rechten +De licentiegever verleent u hierbij een wereldwijde, royaltyvrije, niet-exclusieve, voor een sublicentie in aanmerking komende licentie, om voor de duur van het aan het oorspronkelijke werk verbonden auteursrecht, het volgende te doen: +— het werk in alle omstandigheden en voor ongeacht welk doel te gebruiken; +— het werk te verveelvoudigen; +— het werk te wijzigen en op grond van het werk bewerkingen te ontwikkelen; +— het werk aan het publiek mede te delen, waaronder het recht om het werk of kopieën ervan aan het publiek ter beschikking te stellen of te vertonen, en het werk, in voorkomend geval, in het openbaar uit te voeren; +— het werk of kopieën ervan te verspreiden; +— het werk of kopieën ervan uit te lenen en te verhuren; +— de rechten op het werk of op kopieën ervan in sublicentie te geven. +Deze rechten kunnen worden uitgeoefend met gebruikmaking van alle thans bekende of nog uit te vinden media, dragers en formaten, voor zover het toepasselijke recht dit toestaat. In de landen waar immateriële rechten van toepassing zijn, doet de licentiegever afstand van zijn recht op uitoefening van zijn immateriële rechten in de mate die door het toepasselijke recht wordt toegestaan teneinde een doeltreffende uitoefening van de bovenvermelde in licentie gegeven economische rechten mogelijk te maken. De licentiegever verleent de licentiehouder een royaltyvrij, niet-exclusief gebruiksrecht op alle octrooien van de licentiegever, voor zover dit noodzakelijk is om de uit hoofde van deze licentie verleende rechten op het werk te gebruiken. + +3.Mededeling van de broncode +De licentiegever kan het werk verstrekken in zijn broncode of als uitvoerbare code. Indien het werk als uitvoerbare code wordt verstrekt, verstrekt de licentiegever bij elke door hem verspreide kopie van het werk tevens een machinaal leesbare kopie van de broncode van het werk of geeft hij in een mededeling, volgende op de bij het werk gevoegde auteursrechtelijke kennisgeving, de plaats aan waar de broncode gemakkelijk en vrij toegankelijk is, zolang de licentiegever het werk blijft verspreiden of mededelen. + +4.Beperkingen van het auteursrecht +Geen enkele bepaling in deze licentie heeft ten doel de licentiehouder het recht te ontnemen een beroep te doen op een uitzondering op of een beperking van de exclusieve rechten van de rechthebbenden op het werk, of op de uitputting van die rechten of andere toepasselijke beperkingen daarvan. + +5.Verplichtingen van de licentiehouder +De verlening van de bovenvermelde rechten is onderworpen aan een aantal aan de licentiehouder opgelegde beperkingen en verplichtingen. Het gaat om de onderstaande verplichtingen. + +Attributierecht: de licentiehouder moet alle auteurs-, octrooi- of merkenrechtelijke kennisgevingen onverlet laten alsook alle kennisgevingen die naar de licentie en de afwijzing van garanties verwijzen. De licentiehouder moet een afschrift van deze kennisgevingen en een afschrift van de licentie bij elke kopie van het werk voegen die hij verspreidt of mededeelt. De licentiehouder moet in elke bewerking duidelijk aangeven dat het werk is gewijzigd, en eveneens de datum van wijziging vermelden. + +Copyleftclausule: wanneer de licentiehouder kopieën van het oorspronkelijke werk of bewerkingen verspreidt of mededeelt, geschiedt die verspreiding of mededeling onder de voorwaarden van deze licentie of van een latere versie van deze licentie, tenzij het oorspronkelijke werk uitdrukkelijk alleen onder deze versie van de licentie wordt verspreid — bijvoorbeeld door de mededeling „alleen EUPL v. 1.2”. De licentiehouder (die licentiegever wordt) kan met betrekking tot het werk of de bewerkingen geen aanvullende bepalingen of voorwaarden opleggen of stellen die de voorwaarden van de licentie wijzigen of beperken. + +Verenigbaarheidsclausule: wanneer de licentiehouder bewerkingen of kopieën ervan verspreidt of mededeelt die zijn gebaseerd op het werk en op een ander werk dat uit hoofde van een verenigbare licentie in licentie is gegeven, kan die verspreiding of mededeling geschieden onder de voorwaarden van deze verenigbare licentie. Voor de toepassing van deze clausule wordt onder „verenigbare licentie” verstaan, de licenties die in het aanhangsel bij deze licentie zijn opgesomd. Indien de verplichtingen van de licentiehouder uit hoofde van de verenigbare licentie in strijd zijn met diens verplichtingen uit hoofde van deze licentie, hebben de verplichtingen van de verenigbare licentie voorrang. + +Verstrekking van de broncode: bij de verspreiding of mededeling van kopieën van het werk verstrekt de licentiehouder een machinaal leesbare kopie van de broncode of geeft hij aan waar deze broncode gemakkelijk en vrij toegankelijk is, zolang de licentiehouder het werk blijft verspreiden of mededelen. + +Juridische bescherming: deze licentie verleent geen toestemming om handelsnamen, handelsmerken, dienstmerken of namen van de licentiegever te gebruiken, behalve wanneer dit op grond van een redelijk en normaal gebruik noodzakelijk is om de oorsprong van het werk te beschrijven en de inhoud van de auteursrechtelijke kennisgeving te herhalen. + +6.Auteursketen +De oorspronkelijke licentiegever garandeert dat hij houder is van het hierbij verleende auteursrecht op het oorspronkelijke werk dan wel dat dit hem in licentie is gegeven en dat hij de bevoegdheid heeft de licentie te verlenen. Elke bewerker garandeert dat hij houder is van het auteursrecht op de door hem aan het werk aangebrachte wijzigingen dan wel dat dit hem in licentie is gegeven en dat hij de bevoegdheid heeft de licentie te verlenen. Telkens wanneer u de licentie aanvaardt, verlenen de oorspronkelijke licentiegever en de opeenvolgende bewerkers u een licentie op hun bijdragen aan het werk onder de voorwaarden van deze licentie. + +7.Uitsluiting van garantie +Het werk is een werk in ontwikkeling, dat voortdurend door vele bewerkers wordt verbeterd. Het is een onvoltooid werk, dat bijgevolg nog tekortkomingen of programmeerfouten („bugs”) kan vertonen, die onlosmakelijk verbonden zijn met dit soort ontwikkeling. Om die reden wordt het werk op grond van de licentie verstrekt „zoals het is” en zonder enige garantie met betrekking tot het werk te geven, met inbegrip van, maar niet beperkt tot garanties met betrekking tot de verhandelbaarheid, de geschiktheid voor een specifiek doel, de afwezigheid van tekortkomingen of fouten, de nauwkeurigheid, de eerbiediging van andere intellectuele-eigendomsrechten dan het in artikel 6 van deze licentie bedoelde auteursrecht. Deze uitsluiting van garantie is een essentieel onderdeel van de licentie en een voorwaarde voor de verlening van rechten op het werk. + +8.Uitsluiting van aansprakelijkheid +Behoudens in het geval van een opzettelijke fout of directe schade aan natuurlijke personen, is de licentiegever in geen enkel geval aansprakelijk voor ongeacht welke directe of indirecte, materiële of immateriële schade die voortvloeit uit de licentie of het gebruik van het werk, met inbegrip van, maar niet beperkt tot schade als gevolg van het verlies van goodwill, verloren werkuren, een computerdefect of computerfout, het verlies van gegevens, of enige andere commerciële schade, zelfs indien de licentiegever werd gewezen op de mogelijkheid van dergelijke schade. De licentiegever is echter aansprakelijk op grond van de wetgeving inzake productaansprakelijkheid, voor zover deze wetgeving op het werk van toepassing is. + +9.Aanvullende overeenkomsten +Bij de verspreiding van het werk kunt u ervoor kiezen een aanvullende overeenkomst te sluiten, waarin de verplichtingen of diensten overeenkomstig deze licentie worden omschreven. Indien deze verplichtingen worden aanvaard, kunt u echter alleen in eigen naam en onder eigen verantwoordelijkheid handelen, en dus niet in naam van de oorspronkelijke licentiegever of een bewerker, en kunt u voorts alleen handelen indien u ermee instemt alle bewerkers schadeloos te stellen, te verdedigen of te vrijwaren met betrekking tot de aansprakelijkheid van of vorderingen tegen deze bewerkers op grond van het feit dat u een garantie of aanvullende aansprakelijkheid hebt aanvaard. + +10.Aanvaarding van de licentie +De bepalingen van deze licentie kunnen worden aanvaard door te klikken op het pictogram „Ik ga akkoord”, dat zich bevindt onderaan het venster waarin de tekst van deze licentie is weergegeven, of door overeenkomstig de toepasselijke wetsbepalingen op een soortgelijke wijze met de licentie in te stemmen. Door op dat pictogram te klikken geeft u aan dat u deze licentie en alle voorwaarden ervan ondubbelzinnig en onherroepelijk aanvaardt. Evenzo aanvaardt u onherroepelijk deze licentie en alle voorwaarden ervan door uitoefening van de rechten die u in artikel 2 van deze licentie zijn verleend, zoals het gebruik van het werk, het creëren door u van een bewerking of de verspreiding of mededeling door u van het werk of kopieën ervan. + +11.Voorlichting van het publiek +Indien u het werk verspreidt of mededeelt door middel van elektronische communicatiemiddelen (bijvoorbeeld door voor te stellen het werk op afstand te downloaden), moet het distributiekanaal of het medium (bijvoorbeeld een website) het publiek ten minste de gegevens verschaffen die door het toepasselijke recht zijn voorgeschreven met betrekking tot de licentiegever, de licentie en de wijze waarop deze kan worden geraadpleegd, gesloten, opgeslagen en gereproduceerd door de licentiehouder. + +12.Einde van de licentie +De licentie en de uit hoofde daarvan verleende rechten eindigen automatisch bij elke inbreuk door de licentiehouder op de voorwaarden van de licentie. Dit einde beëindigt niet de licenties van personen die het werk van de licentiehouder krachtens de licentie hebben ontvangen, mits deze personen zich volledig aan de licentie houden. + +13.Overige +Onverminderd artikel 9 vormt de licentie de gehele overeenkomst tussen de partijen met betrekking tot het werk. Indien een bepaling van de licentie volgens het toepasselijke recht ongeldig is of niet uitvoerbaar is, doet dit geen afbreuk aan de geldigheid of uitvoerbaarheid van de licentie in haar geheel. Deze bepaling dient zodanig te worden uitgelegd of gewijzigd dat zij geldig en uitvoerbaar wordt. De Europese Commissie kan, voor zover dit noodzakelijk en redelijk is, versies in andere talen of nieuwe versies van deze licentie of geactualiseerde versies van dit aanhangsel publiceren, zonder de draagwijdte van de uit hoofde van de licentie verleende rechten te beperken. Nieuwe versies van de licentie zullen worden gepubliceerd met een uniek versienummer. Alle door de Europese Commissie goedgekeurde taalversies van deze licentie hebben dezelfde waarde. De partijen kunnen zich beroepen op de taalversie van hun keuze. + +14.Bevoegd gerecht +Onverminderd specifieke overeenkomsten tussen de partijen, +— vallen alle geschillen tussen de instellingen, organen en instanties van de Europese Unie, als licentiegeefster, en een licentiehouder in verband met de uitlegging van deze licentie onder de bevoegdheid van het Hof van Justitie van de Europese Unie, conform artikel 272 van het Verdrag betreffende de werking van de Europese Unie, +— vallen alle geschillen tussen andere partijen in verband met de uitlegging van deze licentie onder de uitsluitende bevoegdheid van het bevoegde gerecht van de plaats waar de licentiegever is gevestigd of zijn voornaamste activiteit uitoefent. + +15.Toepasselijk recht +Onverminderd specifieke overeenkomsten tussen de partijen, +— wordt deze licentie beheerst door het recht van de lidstaat van de Europese Unie waar de licentiegever zijn statutaire zetel, verblijfplaats of hoofdkantoor heeft, +— wordt deze licentie beheerst door het Belgische recht indien de licentiegever geen statutaire zetel, verblijfplaats of hoofdkantoor heeft in een lidstaat van de Europese Unie. + + +Aanhangsel +„Verenigbare licenties” in de zin van artikel 5 EUPL zijn: +— GNU General Public License (GPL) v. 2, v. 3 +— GNU Affero General Public License (AGPL) v. 3 +— Open Software License (OSL) v. 2.1, v. 3.0 +— Eclipse Public License (EPL) v. 1.0 +— CeCILL v. 2.0, v. 2.1 +— Mozilla Public Licence (MPL) v. 2 +— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) voor andere werken dan software +— European Union Public Licence (EUPL) v. 1.1, v. 1.2 +— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) of Strong Reciprocity (LiLiQ-R+). +De Europese Commissie kan dit aanhangsel actualiseren in geval van latere versies van de bovengenoemde licenties zonder dat er een nieuwe EUPL-versie wordt ontwikkeld, zolang die versies de uit hoofde van artikel 2 van deze licentie verleende rechten verlenen en ze de betrokken broncode beschermen tegen exclusieve toe-eigening. +Voor alle andere wijzigingen van of aanvullingen op dit aanhangsel is de ontwikkeling van een nieuwe EUPL-versie vereist. diff --git a/app/Dockerfile b/app/Dockerfile index bea4236f..dd066df7 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -6,19 +6,59 @@ RUN cd /srv/web \ && npm install \ && npm run build -FROM python:3.9.15-slim-buster +FROM python:3.10.10-slim-bullseye EXPOSE 8000 +ENV APP_HOME=/app +ENV APP_USER=appuser +ENV APP_USER_ID=2000 + +RUN groupadd -r $APP_USER && \ + useradd -r -u $APP_USER_ID -g $APP_USER -d $APP_HOME -s /sbin/nologin -c "Docker image user" $APP_USER + ENV PYTHONUNBUFFERED 1 WORKDIR /app/ -RUN apt-get update && apt-get install -y \ - build-essential \ - python-dev \ - git \ - locales \ +RUN apt-get update \ + && apt-get dist-upgrade -y \ + && apt-get autoremove -y \ + && apt-get install --no-install-recommends -y \ + build-essential \ + python-dev \ + curl \ + unzip \ + wget \ + dnsutils \ + vim-tiny \ + net-tools \ + netcat \ + libgeos-c1v5 \ + gdal-bin \ + postgresql-client \ + libgdal28 \ + libspatialite7 \ + libfreexl1 \ + libgeotiff-dev \ + libwebp6 \ + proj-bin \ + mime-support \ + gettext \ + libwebpmux3 \ + libwebpdemux2 \ + libxml2 \ + libfreetype6 \ + libtiff5 \ + libgdk-pixbuf2.0-0 \ + libmagic1 \ + libcairo2 \ + libpango1.0-0 \ + gcc \ + graphviz \ + graphviz-dev \ + git \ + locales \ && pip install --upgrade pip \ && sed -i '/nl_NL.UTF-8/s/^# //g' /etc/locale.gen && \ locale-gen @@ -29,12 +69,21 @@ ENV LANG=nl_NL.UTF-8 \ COPY . /app/ +COPY --from=node_step /srv/web/public/build /static/ -RUN mkdir -p /media && mkdir -p /static && chown 2000 /media && chown 2000 /static && chmod 744 /media && chmod 744 /static \ - && mkdir -p /srv/web/var/cache && chown 2000 /srv/web/var/cache && chmod -R ugo+rwx /srv/web/var/cache \ - && chmod +x /app/deploy/docker-entrypoint.sh && chown root:root /app/deploy/docker-entrypoint.sh \ +RUN mkdir -p /media \ + && mkdir -p /static \ + && chown -R $APP_USER:$APP_USER /media \ + && chown -R $APP_USER:$APP_USER /static \ + && chmod -R 744 /media \ + && chmod -R 744 /static \ + && mkdir -p /srv/web/var/cache \ + && chown $APP_USER:$APP_USER /srv/web/var/cache \ + && chmod -R ugo+rwx /srv/web/var/cache \ + && chown -R $APP_USER:$APP_USER $APP_HOME \ + && chmod -R +x /app/deploy \ && pip install --no-cache-dir -r /app/requirements.txt -COPY --from=node_step /srv/web/public/build /static/ +USER $APP_USER CMD ["bash", "/app/deploy/docker-entrypoint.sh"] diff --git a/app/apps/authenticatie/__init__.py b/app/apps/authenticatie/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/apps/authenticatie/admin.py b/app/apps/authenticatie/admin.py new file mode 100644 index 00000000..0ca76690 --- /dev/null +++ b/app/apps/authenticatie/admin.py @@ -0,0 +1,35 @@ +from apps.authenticatie.models import Gebruiker +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + + +class GebruikerAdmin(UserAdmin): + model = Gebruiker + list_display = ( + "email", + "is_staff", + "is_active", + ) + list_filter = ( + "email", + "is_staff", + "is_active", + ) + fieldsets = ( + (None, {"fields": ("email", "password")}), + ("Permissions", {"fields": ("is_staff", "is_active")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("email", "password1", "password2", "is_staff", "is_active"), + }, + ), + ) + search_fields = ("email",) + ordering = ("email",) + + +admin.site.register(Gebruiker, GebruikerAdmin) diff --git a/app/apps/authenticatie/apps.py b/app/apps/authenticatie/apps.py new file mode 100644 index 00000000..32fa7f9a --- /dev/null +++ b/app/apps/authenticatie/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthenticatieConfig(AppConfig): + name = "apps.authenticatie" + verbose_name = "Authenticatie" diff --git a/app/apps/authenticatie/auth.py b/app/apps/authenticatie/auth.py new file mode 100644 index 00000000..6d7aebfd --- /dev/null +++ b/app/apps/authenticatie/auth.py @@ -0,0 +1,20 @@ +from mozilla_django_oidc import auth + + +class OIDCAuthenticationBackend(auth.OIDCAuthenticationBackend): + def create_user(self, claims): + email = claims.get("email") + user = self.UserModel.objects.create_user(email=email) + + user.first_name = claims.get("given_name", "") + user.last_name = claims.get("family_name", "") + user.save() + + return user + + def update_user(self, user, claims): + user.first_name = claims.get("given_name", "") + user.last_name = claims.get("family_name", "") + user.save() + + return user diff --git a/app/apps/authenticatie/backends.py b/app/apps/authenticatie/backends.py new file mode 100644 index 00000000..eeae03f6 --- /dev/null +++ b/app/apps/authenticatie/backends.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (C) 2022 Delta10 B.V. +from mozilla_django_oidc.auth import OIDCAuthenticationBackend + + +class AuthenticationBackend(OIDCAuthenticationBackend): + def filter_users_by_claims(self, claims): + print("claims") + print(claims) + email = claims.get("email") + if not email: + return self.UserModel.objects.none() + + return self.UserModel.objects.filter(username__iexact=email) + + def create_user(self, claims): + return None # do not create users when they do not exist in the database + + def update_user(self, user, claims): + return user # do not update any attributes in the database based on claims + + def authenticate(self, request, **kwargs): + print("authenticate") + print(request) + return super().authenticate(request, **kwargs) diff --git a/app/apps/authenticatie/managers.py b/app/apps/authenticatie/managers.py new file mode 100644 index 00000000..a2e412cc --- /dev/null +++ b/app/apps/authenticatie/managers.py @@ -0,0 +1,44 @@ +from django.contrib.auth.base_user import BaseUserManager +from django.contrib.auth.hashers import make_password +from django.utils.translation import ugettext_lazy as _ + + +class GebruikerManager(BaseUserManager): + """ + Gebruiker model manager where email is the unique identifiers + for authentication instead of usernames. + """ + + def _create_user(self, email, password, **extra_fields): + """ + Create and save a user with the given username, email, and password. + """ + if not email: + raise ValueError("The given email must be set") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.password = make_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password=None, **extra_fields): + """ + Create and save a User with the given email and password. + """ + extra_fields.setdefault("is_staff", False) + extra_fields.setdefault("is_superuser", False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password=None, **extra_fields): + """ + Create and save a SuperUser with the given email and password. + """ + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_active", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError(_("Superuser must have is_staff=True.")) + if extra_fields.get("is_superuser") is not True: + raise ValueError(_("Superuser must have is_superuser=True.")) + return self._create_user(email, password, **extra_fields) diff --git a/app/apps/authenticatie/migrations/0001_initial.py b/app/apps/authenticatie/migrations/0001_initial.py new file mode 100644 index 00000000..a9068893 --- /dev/null +++ b/app/apps/authenticatie/migrations/0001_initial.py @@ -0,0 +1,107 @@ +# Generated by Django 3.2.16 on 2023-06-29 08:17 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="Gebruiker", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ("email", models.EmailField(max_length=254, unique=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + ), + ] diff --git a/app/apps/authenticatie/migrations/__init__.py b/app/apps/authenticatie/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/apps/authenticatie/models.py b/app/apps/authenticatie/models.py new file mode 100644 index 00000000..d028ada7 --- /dev/null +++ b/app/apps/authenticatie/models.py @@ -0,0 +1,16 @@ +from apps.authenticatie.managers import GebruikerManager +from django.contrib.auth.models import AbstractUser +from django.contrib.gis.db import models + + +class Gebruiker(AbstractUser): + username = None + email = models.EmailField(unique=True) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + objects = GebruikerManager() + + def __str__(self): + return self.email diff --git a/app/apps/authenticatie/templates/auth/check_sso_status.html b/app/apps/authenticatie/templates/auth/check_sso_status.html new file mode 100644 index 00000000..3083ca4e --- /dev/null +++ b/app/apps/authenticatie/templates/auth/check_sso_status.html @@ -0,0 +1,20 @@ + + diff --git a/app/apps/authenticatie/views.py b/app/apps/authenticatie/views.py new file mode 100644 index 00000000..44b5d81b --- /dev/null +++ b/app/apps/authenticatie/views.py @@ -0,0 +1,32 @@ +import logging +from urllib import parse + +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + + +def provider_logout(request): + logout_url = settings.OIDC_OP_LOGOUT_ENDPOINT + oidc_id_token = request.session.get("oidc_id_token", None) + redirect_url = request.build_absolute_uri( + location=request.GET.get("next", settings.LOGOUT_REDIRECT_URL) + ) + if oidc_id_token: + logout_url = ( + settings.OIDC_OP_LOGOUT_ENDPOINT + + "?" + + parse.urlencode( + { + "id_token_hint": oidc_id_token, + "post_logout_redirect_uri": redirect_url, + } + ) + ) + logout_response = requests.get(logout_url) + if logout_response.status_code != 200: + logger.error( + f"provider_logout: status code: {logout_response.status_code}, logout_url: {logout_url}" + ) + return redirect_url diff --git a/app/apps/health/__init__.py b/app/apps/health/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/apps/health/apps.py b/app/apps/health/apps.py new file mode 100644 index 00000000..73230018 --- /dev/null +++ b/app/apps/health/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig +from health_check.plugins import plugin_dir + + +class HealthConfig(AppConfig): + name = "apps.health" + verbose_name = "Health" + + def ready(self): + from apps.health.custom_checks import MeldingenAPIHealthCheck + + plugin_dir.register(MeldingenAPIHealthCheck) diff --git a/app/apps/health/custom_checks.py b/app/apps/health/custom_checks.py new file mode 100644 index 00000000..602b1526 --- /dev/null +++ b/app/apps/health/custom_checks.py @@ -0,0 +1,23 @@ +import requests +from django.conf import settings +from health_check.backends import BaseHealthCheckBackend +from health_check.exceptions import HealthCheckException + + +class MeldingenAPIHealthCheck(BaseHealthCheckBackend): + critical_service = False + + def check_status(self): + health_check_response = requests.get(settings.MELDINGEN_API_HEALTH_CHECK_URL) + + if health_check_response.status_code != 200: + raise HealthCheckException( + f"Meldingen API not ready: status code: {health_check_response.status_code}" + ) + if health_check_response.status_code == 404: + raise HealthCheckException( + f"Meldingen API: health url not implemented: status code: {health_check_response.status_code}" + ) + + def identifier(self): + return self.__class__.__name__ diff --git a/app/apps/meldingen/__init__.py b/app/apps/meldingen/__init__.py new file mode 100644 index 00000000..426fd2e1 --- /dev/null +++ b/app/apps/meldingen/__init__.py @@ -0,0 +1,4 @@ +from apps.meldingen.service import MeldingenService +from django.conf import settings + +service_instance = MeldingenService(settings.MELDINGEN_URL) diff --git a/app/apps/meldingen/service.py b/app/apps/meldingen/service.py new file mode 100644 index 00000000..ebaea7fe --- /dev/null +++ b/app/apps/meldingen/service.py @@ -0,0 +1,180 @@ +from urllib.parse import urlparse + +import requests +from apps.meldingen.utils import get_meldingen_token +from requests import Request, Response + + +class MeldingenService: + _api_base_url = None + _timeout: tuple[int, ...] = (5, 10) + _api_path: str = "/api/v1" + + class BasisUrlFout(Exception): + ... + + class AntwoordFout(Exception): + ... + + def __init__(self, api_base_url: str, *args, **kwargs: dict): + self._api_base_url = api_base_url.strip().rstrip("/") + super().__init__(*args, **kwargs) + + def get_url(self, url): + url_o = urlparse(url) + if not url_o.scheme and not url_o.netloc: + return f"{self._api_base_url}{url}" + if f"{url_o.scheme}://{url_o.netloc}" == self._api_base_url: + return url + raise MeldingenService.BasisUrlFout( + f"url: {url}, basis_url: {self._api_base_url}" + ) + + def get_headers(self): + headers = {"Authorization": f"Token {get_meldingen_token()}"} + return headers + + def do_request(self, url, method="get", data={}, raw_response=True): + + action: Request = getattr(requests, method) + action_params: dict = { + "url": self.get_url(url), + "headers": self.get_headers(), + "json": data, + "timeout": self._timeout, + } + response: Response = action(**action_params) + + if raw_response: + return response + try: + return response.json() + except Exception: + raise MeldingenService.AntwoordFout( + f"url: {self.get_url(url)}, status code: {response.status_code}, tekst: {response.text}" + ) + + def get_melding_lijst(self, query_string=""): + return self.do_request( + f"{self._api_path}/melding/?{query_string}", + raw_response=False, + ) + + def get_melding(self, id, query_string="") -> dict: + return self.do_request( + f"{self._api_path}/melding/{id}/?{query_string}", + raw_response=False, + ) + + def melding_gebeurtenis_toevoegen( + self, + id, + bijlagen=[], + omschrijving_intern=None, + omschrijving_extern=None, + gebruiker=None, + ): + data = { + "bijlagen": bijlagen, + "omschrijving_intern": omschrijving_intern, + "omschrijving_extern": omschrijving_extern, + "gebruiker": gebruiker, + } + response = self.do_request( + f"{self._api_path}/melding/{id}/gebeurtenis-toevoegen/", + method="post", + data=data, + ) + if response.status_code != 200: + raise MeldingenService.AntwoordFout( + f"status code: {response.status_code}, status code verwacht: 200" + ) + return response.json() + + def melding_status_aanpassen( + self, + id, + status=None, + resolutie=None, + bijlagen=[], + omschrijving_extern=None, + omschrijving_intern=None, + gebruiker=None, + ): + data = { + "bijlagen": bijlagen, + "omschrijving_extern": omschrijving_extern, + "omschrijving_intern": omschrijving_intern, + "gebruiker": gebruiker, + } + if status: + data.update( + { + "status": { + "naam": status, + }, + "resolutie": resolutie, + } + ) + return self.do_request( + f"{self._api_path}/melding/{id}/status-aanpassen/" + if status + else f"{self._api_path}/melding/{id}/gebeurtenis-toevoegen/", + method="patch" if status else "post", + data=data, + raw_response=False, + ) + + def taakapplicaties(self): + return self.do_request( + f"{self._api_path}/taakapplicatie/", + raw_response=False, + ) + + def taak_aanmaken( + self, + melding_uuid, + taaktype_url, + titel, + bericht=None, + gebruiker=None, + additionele_informatie={}, + ): + data = { + "taaktype": taaktype_url, + "titel": titel, + "bericht": bericht, + "gebruiker": gebruiker, + "additionele_informatie": additionele_informatie, + } + return self.do_request( + f"{self._api_path}/melding/{melding_uuid}/taakopdracht/", + method="post", + data=data, + raw_response=False, + ) + + def taak_status_aanpassen( + self, + taakopdracht_url, + status, + resolutie=None, + omschrijving_intern=None, + bijlagen=None, + gebruiker=None, + ): + data = { + "taakstatus": { + "naam": status, + }, + "resolutie": resolutie, + "omschrijving_intern": omschrijving_intern, + "bijlagen": bijlagen, + "gebruiker": gebruiker, + } + return self.do_request( + f"{taakopdracht_url}status-aanpassen/", + method="patch", + data=data, + raw_response=False, + ) diff --git a/app/apps/meldingen/utils.py b/app/apps/meldingen/utils.py new file mode 100644 index 00000000..fecc76c6 --- /dev/null +++ b/app/apps/meldingen/utils.py @@ -0,0 +1,60 @@ +import requests +from django.conf import settings +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.validators import validate_email + + +class MeldingAuthResponseException(Exception): + pass + + +def get_meldingen_token(): + meldingen_token = cache.get("meldingen_token2") + if not meldingen_token: + email = settings.MELDINGEN_USERNAME + try: + validate_email(email) + except ValidationError: + email = f"{settings.MELDINGEN_USERNAME}@forzamor.nl" + token_response = requests.post( + settings.MELDINGEN_TOKEN_API, + json={ + "username": email, + "password": settings.MELDINGEN_PASSWORD, + }, + ) + if token_response.status_code == 200: + meldingen_token = token_response.json().get("token") + cache.set( + "meldingen_token", meldingen_token, settings.MELDINGEN_TOKEN_TIMEOUT + ) + else: + raise MeldingAuthResponseException( + f"auth response status code: {token_response.status_code}" + ) + return meldingen_token + + +def get_taaktypes(melding): + from apps.meldingen import service_instance + + taakapplicaties = service_instance.taakapplicaties() + taaktypes = [ + [ + tt.get("_links", {}).get("self"), + f"{tt.get('omschrijving')}", + ] + for ta in taakapplicaties.get("results", []) + for tt in ta.get("taaktypes", []) + ] + gebruikte_taaktypes = [ + *set( + list( + to.get("taaktype") + for to in melding.get("taakopdrachten_voor_melding", []) + ) + ) + ] + taaktypes = [tt for tt in taaktypes if tt[0] not in gebruikte_taaktypes] + return taaktypes diff --git a/app/apps/regie/constanten.py b/app/apps/regie/constanten.py new file mode 100644 index 00000000..52a287dc --- /dev/null +++ b/app/apps/regie/constanten.py @@ -0,0 +1,37 @@ +VERTALINGEN = { + "standaard": "Standaard", + "status_wijziging": "Status wijziging", + "melding_aangemaakt": "Melding aangemaakt", + "taakopdracht_aangemaakt": "Taakopdracht aangemaakt", + "taakopdracht_status_wijziging": "Taakopdracht status wijziging", + "opgelost": "Opgelost", + "niet_opgelost": "Niet opgelost", + "aangemaakt": "Aangemaakt", + "gewijzigd": "Gewijzigd", + "verwijderd": "Verwijderd", + "verwijderd": "Verwijderd", + "in_behandeling": "In behandeling", + "openstaand": "Openstaand", + "controle": "Controle", + "afgehandeld": "Afgehandeld", + "nieuw": "Nieuw", + "bezig": "Bezig", + "voltooid": "Voltooid", + "adres": "Adres", + "lichtmast": "Lichtmast", + "graf": "Graf", + "vak": "Vak", + "fotos": "Foto's", + "aannemer": "Aangenomen door", + "no_email": "De melder beschikt niet over een e-mailadres.", + "grafnummer": "Grafnummer", + "naam_melder": "Naam", + "toelichting": "Toelichting", + "email_melder": "E-mailadres", + "begraafplaats": "Begraafplaats", + "rechthebbende": "Is deze persoon de rechthebbende of belanghebbende?", + "specifiek_graf": "Betreft het verzoek een specifiek graf?", + "naam_overledene": "Naam overledene", + "telefoon_melder": "Telefoonnummer", + "terugkoppeling_gewenst": "Is terugkoppeling gewenst?", +} diff --git a/app/apps/regie/forms.py b/app/apps/regie/forms.py new file mode 100644 index 00000000..92bf4042 --- /dev/null +++ b/app/apps/regie/forms.py @@ -0,0 +1,227 @@ +from django import forms + +BEHANDEL_OPTIES = ( + ( + "ja", + "Ja", + "We zijn met uw melding aan de slag gegaan en hebben het probleem opgelost.", + "afgehandeld", + "opgelost", + ), + ( + "nee", + "Nee", + "We zijn met uw melding aan de slag gegaan maar deze kan niet direct worden opgelost. Want...", + "afgehandeld", + None, + ), +) +TAAK_BEHANDEL_OPTIES = ( + ( + "ja", + "Ja", + "We zijn met uw melding aan de slag gegaan en hebben het probleem opgelost.", + "voltooid", + "opgelost", + ), + ( + "nee", + "Nee, het probleem kan niet worden opgelost.", + "We zijn met uw melding aan de slag gegaan, maar konden het probleem helaas niet oplossen. Want...", + "voltooid", + None, + ), +) + +TAAK_BEHANDEL_STATUS = {bo[0]: bo[3] for bo in TAAK_BEHANDEL_OPTIES} +TAAK_BEHANDEL_RESOLUTIE = {bo[0]: bo[4] for bo in TAAK_BEHANDEL_OPTIES} +BEHANDEL_STATUS = {bo[0]: bo[3] for bo in BEHANDEL_OPTIES} +BEHANDEL_RESOLUTIE = {bo[0]: bo[4] for bo in BEHANDEL_OPTIES} + + +class CheckboxSelectMultipleThumb(forms.CheckboxSelectMultiple): + ... + + +class FilterForm(forms.Form): + + begraafplaats = forms.MultipleChoiceField( + label="Locatie", + widget=forms.CheckboxSelectMultiple( + attrs={ + "class": "list--form-check-input", + "hideLabel": True, + } + ), + choices=[], + required=False, + ) + + ordering = forms.CharField( + widget=forms.HiddenInput(), + initial="aangemaakt_op", + required=False, + ) + + offset = forms.ChoiceField( + widget=forms.RadioSelect( + attrs={ + "class": "list--form-check-input", + "hideLabel": True, + } + ), + required=False, + ) + + limit = forms.CharField( + widget=forms.HiddenInput, + initial="10", + required=False, + ) + + def __init__(self, *args, **kwargs): + offset_options = kwargs.pop("offset_options", None) + locatie_opties = kwargs.pop("locatie_opties", None) + super().__init__(*args, **kwargs) + self.fields["offset"].choices = offset_options + self.fields["begraafplaats"].choices = locatie_opties + + +class LoginForm(forms.Form): + username = forms.CharField(label="Personeelsnummer", widget=forms.TextInput()) + password = forms.CharField(label="Wachtwoord", widget=forms.PasswordInput()) + + +class RadioSelect(forms.RadioSelect): + option_template_name = "widgets/radio_option.html" + + +class InformatieToevoegenForm(forms.Form): + opmerking = forms.CharField( + label="Voeg een opmerking toe", + widget=forms.Textarea( + attrs={"class": "form-control", "data-testid": "information", "rows": "4"} + ), + required=False, + ) + + bijlagen_extra = forms.FileField( + widget=forms.widgets.FileInput( + attrs={ + "accept": ".jpg, .jpeg, .png, .heic", + "data-action": "change->bijlagen#updateImageDisplay", + "data-bijlagen-target": "bijlagenExtra", + } + ), + label="Voeg één of meerdere foto's toe", + required=False, + ) + + +class TaakStartenForm(forms.Form): + taaktype = forms.ChoiceField( + widget=forms.Select(), + label="Taak", + choices=( + ("graf_ophogen", "Graf ophogen"), + ("steen_rechtzetten", "Steen rechtzetten"), + ("snoeien", "Snoeien"), + ), + required=True, + ) + + bericht = forms.CharField( + label="Interne opmerking", + help_text="Deze tekst wordt niet naar de melder verstuurd.", + widget=forms.Textarea( + attrs={"class": "form-control", "data-testid": "information", "rows": "4"} + ), + required=False, + ) + + def __init__(self, *args, **kwargs): + taaktypes = kwargs.pop("taaktypes", None) + super().__init__(*args, **kwargs) + self.fields["taaktype"].choices = taaktypes + + +class TaakAfrondenForm(forms.Form): + status = forms.ChoiceField( + widget=RadioSelect( + attrs={ + "class": "list--form-radio-input", + } + ), + label="Is het probleem opgelost?", + choices=[[x[0], x[1]] for x in TAAK_BEHANDEL_OPTIES], + required=True, + ) + + bijlagen = forms.FileField( + widget=forms.widgets.FileInput( + attrs={ + "accept": ".jpg, .jpeg, .png, .heic", + "data-action": "change->bijlagen#updateImageDisplay", + "data-bijlagen-target": "bijlagenAfronden", + } + ), + label="Foto's", + required=False, + ) + + omschrijving_intern = forms.CharField( + label="Interne opmerking", + help_text="Je kunt deze tekst aanpassen of eigen tekst toevoegen.", + widget=forms.Textarea( + attrs={ + "class": "form-control", + "data-testid": "information", + "rows": "4", + "data-meldingbehandelformulier-target": "internalText", + } + ), + required=False, + ) + + +class MeldingAfhandelenForm(forms.Form): + + omschrijving_extern = forms.CharField( + label="Bericht voor de melder", + help_text="Je kunt deze tekst aanpassen of eigen tekst toevoegen.", + widget=forms.Textarea( + attrs={ + "class": "form-control", + "data-testid": "message", + "rows": "4", + "data-meldingbehandelformulier-target": "externalText", + } + ), + required=False, + ) + + omschrijving_intern = forms.CharField( + label="Interne opmerking", + help_text="Deze tekst wordt niet naar de melder verstuurd.", + widget=forms.Textarea( + attrs={ + "class": "form-control", + "rows": "4", + "data-meldingbehandelformulier-target": "internalText", + } + ), + required=False, + ) + + def __init__(self, *args, **kwargs): + # bijlagen = kwargs.pop("bijlagen", None) + super().__init__(*args, **kwargs) + # print("FORMS bijlagen = = = >") + # print(bijlagen) + # self.fields["bijlagen"].choices = bijlagen + # self.fields["bijlagen"].choices = [ + # (str(m.get("afbeelding")), m.get("afbeelding")) for m in bijlagen + # ] + + # def terugsturen(self, data): + # if data.get("afhandel_reden") == "" diff --git a/app/apps/regie/mock.py b/app/apps/regie/mock.py index 93bb29c0..2a7bb1e6 100644 --- a/app/apps/regie/mock.py +++ b/app/apps/regie/mock.py @@ -2,9 +2,10 @@ { "id": 58, "uuid": "567be434-991d-40db-8452-759182e7433b", - "aangemaakt_op": "2014-01-16T04:20:17 -01:00", - "aangepast_op": "2020-06-08T12:59:07 -02:00", - "origineel_aangemaakt": "2017-01-16T07:04:51 -01:00", + "aangemaakt_op": "2014-01-16T04:20:17", + "aangepast_op": "2020-06-08T12:59:07", + "afgesloten_op": "2023-06-08T13:00:00", + "origineel_aangemaakt": "2017-01-16T07:04:51", "tekst": "Cupidatat esse do sunt in. Officia sunt voluptate excepteur fugiat elit sint id est. Eu veniam amet enim esse voluptate aute labore magna aute eiusmod. Minim nisi ipsum est dolore dolore labore do. Nisi et do laboris consectetur proident nostrud quis in culpa aliquip culpa minim consequat qui. Aute eiusmod adipisicing consequat dolor enim laboris consequat dolore cillum dolor dolore. Veniam ipsum pariatur pariatur do culpa occaecat pariatur excepteur.\r\n", "meta": { "naam_overledene": "Roslyn Dickerson", @@ -25,7 +26,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Pernis", - "grafnummer": "'287'", + "grafnummer": "287", "vak": "E", "geometrieen": [], } @@ -46,13 +47,13 @@ { "id": 253, "uuid": "21e5ec23-ef1f-4bee-90c5-8e0834c39c58", - "aangemaakt_op": "2020-03-02T08:42:48 -01:00", - "aangepast_op": "2022-08-03T04:48:43 -02:00", - "origineel_aangemaakt": "2019-08-16T07:22:42 -02:00", + "aangemaakt_op": "2020-03-02T08:42:48", + "afgesloten_op": "2023-06-08T13:00:00", + "origineel_aangemaakt": "2019-08-16T07:22:42", "tekst": "Qui dolor labore qui commodo nostrud. Et consequat officia adipisicing ea sint est proident dolor. Fugiat cillum nostrud non enim. Pariatur ad reprehenderit non cillum aliquip cupidatat ea eiusmod adipisicing laboris et. Ex laborum ullamco commodo ipsum cillum. Sint occaecat voluptate commodo ea exercitation ullamco tempor nisi in cupidatat laboris dolore ullamco incididunt.\r\n", "meta": { "naam_overledene": "Knight Merritt", - "categorie": ["Verzakking eigen graf"], + "categorie": ["Verzakking eigen graf", "Muizen"], "omschrijving_andere_oorzaken": "Cillum officia nulla dolore nostrud. Anim quis nulla excepteur fugiat ad aliqua et. Laboris ullamco magna sunt nostrud esse dolore amet. Esse ipsum laboris fugiat ad. Ad fugiat enim non sunt irure nostrud elit ad occaecat voluptate ut laborum elit. Laboris adipisicing ipsum id sint enim reprehenderit irure consectetur dolore tempor duis pariatur fugiat ex.\r\n", "aannemer": "Aimee Harmon", "rechthebbende": "Nee", @@ -69,14 +70,14 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oudeland, Hoogvliet", - "grafnummer": "'192'", + "grafnummer": "192", "vak": "E", "geometrieen": [], } ], "bijlagen": [ { - "bestand": "http://placehold.it/32x32", + "bestand": "http://placehold.it/64x48", "mimetype": "image/jpeg", "is_afbeelding": True, }, @@ -90,9 +91,9 @@ { "id": 252, "uuid": "cd1e8faa-7fa1-4275-bb1b-59604fc8bc07", - "aangemaakt_op": "2017-06-02T11:10:27 -02:00", - "aangepast_op": "2021-02-23T02:58:51 -01:00", - "origineel_aangemaakt": "2017-06-24T10:19:13 -02:00", + "aangemaakt_op": "2017-06-02T11:10:27", + "aangepast_op": "2021-02-23T02:58:51", + "origineel_aangemaakt": "2017-06-24T10:19:13", "tekst": "Aliquip aute amet magna culpa voluptate dolor est. Aliquip eu irure commodo culpa. Culpa nisi exercitation adipisicing reprehenderit sunt sunt do commodo cupidatat excepteur. Eiusmod consectetur et proident ad laborum reprehenderit ullamco officia. Nulla veniam sunt exercitation est do et adipisicing amet velit. Voluptate deserunt ullamco officia laboris fugiat. Commodo id est sunt consectetur.\r\n", "meta": { "naam_overledene": "Guerrero Dejesus", @@ -113,7 +114,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oudeland, Hoogvliet", - "grafnummer": "'299'", + "grafnummer": "299", "vak": "E", "geometrieen": [], } @@ -129,9 +130,9 @@ { "id": 207, "uuid": "5e6a89f3-f635-4e04-b255-aab946b21963", - "aangemaakt_op": "2020-03-05T04:25:37 -01:00", - "aangepast_op": "2015-11-29T06:17:46 -01:00", - "origineel_aangemaakt": "2021-06-29T09:27:21 -02:00", + "aangemaakt_op": "2020-03-05T04:25:37", + "aangepast_op": "2015-11-29T06:17:46", + "origineel_aangemaakt": "2021-06-29T09:27:21", "tekst": "Ex mollit elit ex quis laboris dolore non exercitation consequat. Cillum cupidatat irure aute et. Cillum cupidatat anim nulla proident et sint fugiat dolor. Laborum enim reprehenderit pariatur amet qui sunt incididunt excepteur ipsum quis in enim. Ex id minim aute excepteur dolor labore et irure. Est Lorem dolor eu ad sint tempor sit officia ut velit. Non deserunt aliqua reprehenderit ex aliqua dolore qui laboris.\r\n", "meta": { "naam_overledene": "Anthony Roth", @@ -152,7 +153,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Pernis", - "grafnummer": "'119'", + "grafnummer": "119", "vak": "C", "geometrieen": [], } @@ -173,9 +174,9 @@ { "id": 360, "uuid": "13c461a7-94dd-4cbe-9d4b-d81399526ec6", - "aangemaakt_op": "2022-09-28T02:40:30 -02:00", - "aangepast_op": "2021-01-20T06:42:30 -01:00", - "origineel_aangemaakt": "2022-09-28T10:13:04 -02:00", + "aangemaakt_op": "2022-09-28T02:40:30", + "aangepast_op": "2021-01-20T06:42:30", + "origineel_aangemaakt": "2022-09-28T10:13:04", "tekst": "Consectetur veniam nostrud anim veniam. Quis adipisicing exercitation aliquip ut non in pariatur aliquip laborum ut culpa ad. Esse magna deserunt esse laboris culpa culpa occaecat adipisicing sint aliqua. Ad deserunt ipsum fugiat aliquip ad minim. Ipsum quis consectetur officia ut amet enim officia sunt eu minim in exercitation proident.\r\n", "meta": { "naam_overledene": "Katherine Potts", @@ -196,7 +197,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Overschie", - "grafnummer": "'255'", + "grafnummer": "255", "vak": "G", "geometrieen": [], } @@ -212,9 +213,9 @@ { "id": 9, "uuid": "9fa6359c-385b-48be-b48e-514290720f81", - "aangemaakt_op": "2021-11-30T01:21:05 -01:00", - "aangepast_op": "2019-03-23T12:30:29 -01:00", - "origineel_aangemaakt": "2014-01-08T09:12:44 -01:00", + "aangemaakt_op": "2021-11-30T01:21:05", + "aangepast_op": "2019-03-23T12:30:29", + "origineel_aangemaakt": "2014-01-08T09:12:44", "tekst": "Id laboris velit nostrud voluptate eu anim occaecat sunt. Laborum sunt ipsum officia minim ex sunt aute nisi adipisicing eiusmod ipsum tempor irure. Elit nisi exercitation occaecat esse commodo dolore ad. Deserunt aliquip cupidatat cillum occaecat amet dolor velit ipsum magna sunt in aute eu. Ut ea occaecat nisi nisi ut in sit culpa in.\r\n", "meta": { "naam_overledene": "Terry Knight", @@ -235,7 +236,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Hoek van Holland", - "grafnummer": "'196'", + "grafnummer": "196", "vak": "D", "geometrieen": [], } @@ -281,9 +282,9 @@ { "id": 393, "uuid": "2cc5e5e0-5719-4233-a150-e81245a220e0", - "aangemaakt_op": "2018-06-17T08:35:33 -02:00", - "aangepast_op": "2017-09-21T09:21:31 -02:00", - "origineel_aangemaakt": "2016-02-29T09:52:05 -01:00", + "aangemaakt_op": "2018-06-17T08:35:33", + "aangepast_op": "2017-09-21T09:21:31", + "origineel_aangemaakt": "2016-02-29T09:52:05", "tekst": "Deserunt mollit deserunt velit ea officia laboris in in eiusmod sint voluptate culpa ut. Cupidatat ad duis anim velit commodo cupidatat aliqua exercitation minim. Ad eiusmod ipsum consequat velit est sit. Lorem aute consequat eiusmod in velit adipisicing exercitation cupidatat aliquip exercitation cupidatat.\r\n", "meta": { "naam_overledene": "Harriett Wheeler", @@ -304,7 +305,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Pernis", - "grafnummer": "'83'", + "grafnummer": "83", "vak": "D", "geometrieen": [], } @@ -320,9 +321,9 @@ { "id": 40, "uuid": "8720d519-97b7-4093-8e31-c9f96df3abed", - "aangemaakt_op": "2015-12-01T09:44:41 -01:00", - "aangepast_op": "2020-05-03T06:34:09 -02:00", - "origineel_aangemaakt": "2019-03-31T03:26:08 -02:00", + "aangemaakt_op": "2015-12-01T09:44:41", + "aangepast_op": "2020-05-03T06:34:09", + "origineel_aangemaakt": "2019-03-31T03:26:08", "tekst": "Eu officia labore aliqua eiusmod consequat cupidatat ullamco. Et aute esse enim id laboris exercitation culpa do et voluptate aliquip est. Cupidatat ea ex excepteur reprehenderit esse consequat. Cillum est amet cillum voluptate labore proident amet sint ipsum. Lorem do dolore minim eiusmod velit deserunt ex pariatur tempor id qui sint nostrud. Ad nostrud dolore tempor tempor adipisicing nulla qui. Adipisicing ut aliquip exercitation culpa occaecat commodo consectetur mollit consectetur consequat elit.\r\n", "meta": { "naam_overledene": "Humphrey Morton", @@ -343,7 +344,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Schiebroek", - "grafnummer": "'124'", + "grafnummer": "124", "vak": "A", "geometrieen": [], } @@ -359,9 +360,9 @@ { "id": 158, "uuid": "e69797e1-b6d6-449e-90cc-c00104bd24fa", - "aangemaakt_op": "2022-09-06T12:38:06 -02:00", - "aangepast_op": "2019-04-08T12:29:19 -02:00", - "origineel_aangemaakt": "2017-04-10T06:28:26 -02:00", + "aangemaakt_op": "2022-09-06T12:38:06", + "aangepast_op": "2019-04-08T12:29:19", + "origineel_aangemaakt": "2017-04-10T06:28:26", "tekst": "Tempor commodo laboris cupidatat duis exercitation ullamco dolore id excepteur nulla. Nisi eiusmod qui labore voluptate. Mollit voluptate fugiat est do. Irure est exercitation veniam anim et quis sunt laboris Lorem ad ad. Ad Lorem ipsum sunt aliqua ad elit occaecat ad dolor nulla fugiat. In ex sit id non cupidatat commodo sunt officia Lorem exercitation laboris adipisicing ad.\r\n", "meta": { "naam_overledene": "Elliott Forbes", @@ -382,7 +383,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Pernis", - "grafnummer": "'8'", + "grafnummer": "8", "vak": "C", "geometrieen": [], } @@ -398,9 +399,9 @@ { "id": 330, "uuid": "a12b269c-0ca6-4eb3-a46d-9b13d8334430", - "aangemaakt_op": "2019-11-08T04:50:17 -01:00", - "aangepast_op": "2023-03-08T03:39:33 -01:00", - "origineel_aangemaakt": "2014-02-13T11:16:34 -01:00", + "aangemaakt_op": "2019-11-08T04:50:17", + "aangepast_op": "2023-03-08T03:39:33", + "origineel_aangemaakt": "2014-02-13T11:16:34", "tekst": "Consequat ullamco amet reprehenderit tempor laboris est cupidatat exercitation culpa enim occaecat cillum enim adipisicing. Sit mollit est minim nulla. Tempor enim ullamco quis veniam pariatur elit dolore proident esse id esse nisi tempor reprehenderit. Exercitation in deserunt labore ex.\r\n", "meta": { "naam_overledene": "Kristie Terry", @@ -421,7 +422,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Rozenburg", - "grafnummer": "'152'", + "grafnummer": "152", "vak": "C", "geometrieen": [], } @@ -442,9 +443,9 @@ { "id": 307, "uuid": "16cbc6f0-effd-44a9-aba6-e5e22021e970", - "aangemaakt_op": "2022-03-29T06:35:04 -02:00", - "aangepast_op": "2020-10-22T02:53:22 -02:00", - "origineel_aangemaakt": "2023-02-25T10:26:24 -01:00", + "aangemaakt_op": "2022-03-29T06:35:04", + "aangepast_op": "2020-10-22T02:53:22", + "origineel_aangemaakt": "2023-02-25T10:26:24", "tekst": "Consequat laborum duis incididunt adipisicing velit esse laboris laboris. Laboris mollit nostrud voluptate anim velit ipsum occaecat velit dolore quis. Fugiat adipisicing adipisicing eiusmod commodo eu irure exercitation adipisicing sint do occaecat sint cupidatat aute. Elit ipsum aute et quis sit est pariatur deserunt duis. Enim incididunt ad do duis laboris amet ad fugiat.\r\n", "meta": { "naam_overledene": "Sherrie Sanford", @@ -465,7 +466,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats en crematorium Hofwijk", - "grafnummer": "'262'", + "grafnummer": "262", "vak": "D", "geometrieen": [], } @@ -481,9 +482,9 @@ { "id": 223, "uuid": "b5d266f9-7555-4bf2-9727-b4ba5b49892a", - "aangemaakt_op": "2021-08-07T07:14:34 -02:00", - "aangepast_op": "2017-12-09T07:02:26 -01:00", - "origineel_aangemaakt": "2014-10-16T01:39:19 -02:00", + "aangemaakt_op": "2021-08-07T07:14:34", + "aangepast_op": "2017-12-09T07:02:26", + "origineel_aangemaakt": "2014-10-16T01:39:19", "tekst": "Commodo ullamco veniam sit do laborum et dolor ex sunt sit aliquip laborum. Ex qui dolore eiusmod ea in eiusmod sint. Ut eiusmod veniam pariatur aliquip laborum culpa.\r\n", "meta": { "naam_overledene": "Copeland Cooke", @@ -504,7 +505,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Hoek van Holland", - "grafnummer": "'27'", + "grafnummer": "27", "vak": "E", "geometrieen": [], } @@ -525,9 +526,9 @@ { "id": 95, "uuid": "ffdec5fd-5634-456f-8332-994f55f49fad", - "aangemaakt_op": "2021-07-24T08:02:57 -02:00", - "aangepast_op": "2022-01-29T11:36:57 -01:00", - "origineel_aangemaakt": "2020-07-31T02:15:24 -02:00", + "aangemaakt_op": "2021-07-24T08:02:57", + "aangepast_op": "2022-01-29T11:36:57", + "origineel_aangemaakt": "2020-07-31T02:15:24", "tekst": "In ipsum incididunt irure est occaecat ullamco dolor consectetur occaecat nisi eu exercitation incididunt. Aliquip est in veniam pariatur aute ea sint. Laboris mollit est tempor aliquip commodo sint velit consectetur. Fugiat adipisicing esse et tempor voluptate minim voluptate mollit esse exercitation. Veniam nulla voluptate elit reprehenderit velit ipsum veniam veniam dolore consectetur.\r\n", "meta": { "naam_overledene": "Neva Keller", @@ -548,7 +549,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Crooswijk", - "grafnummer": "'230'", + "grafnummer": "230", "vak": "B", "geometrieen": [], } @@ -564,9 +565,9 @@ { "id": 438, "uuid": "6f0d6d7e-c110-4173-8214-a8d0f63e6b39", - "aangemaakt_op": "2022-12-15T04:37:09 -01:00", - "aangepast_op": "2020-03-21T07:57:14 -01:00", - "origineel_aangemaakt": "2022-04-23T03:54:12 -02:00", + "aangemaakt_op": "2022-12-15T04:37:09", + "aangepast_op": "2020-03-21T07:57:14", + "origineel_aangemaakt": "2022-04-23T03:54:12", "tekst": "Id proident magna amet qui occaecat irure sunt nisi sint magna anim ex labore reprehenderit. Dolor commodo eiusmod nisi et cillum labore. Commodo nisi commodo adipisicing id sint fugiat incididunt.\r\n", "meta": { "naam_overledene": "Wong Barlow", @@ -587,7 +588,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oudeland, Hoogvliet", - "grafnummer": "'39'", + "grafnummer": "39", "vak": "G", "geometrieen": [], } @@ -603,9 +604,9 @@ { "id": 138, "uuid": "eb40db4f-5f0b-47af-a321-4ecc0de3cafa", - "aangemaakt_op": "2014-03-22T07:46:51 -01:00", - "aangepast_op": "2022-07-27T12:50:53 -02:00", - "origineel_aangemaakt": "2020-11-12T08:19:11 -01:00", + "aangemaakt_op": "2014-03-22T07:46:51", + "aangepast_op": "2022-07-27T12:50:53", + "origineel_aangemaakt": "2020-11-12T08:19:11", "tekst": "Sint pariatur commodo voluptate officia ipsum quis adipisicing magna fugiat id eiusmod occaecat minim magna. Ea est anim occaecat ex quis sit in veniam voluptate mollit consequat elit. Ut sint laboris mollit ad ea adipisicing laborum ad adipisicing in ad sunt officia. Culpa in Lorem tempor fugiat eiusmod anim nostrud voluptate ex laboris sunt excepteur mollit adipisicing. Enim tempor commodo occaecat labore.\r\n", "meta": { "naam_overledene": "Hernandez Singleton", @@ -626,7 +627,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Crooswijk", - "grafnummer": "'91'", + "grafnummer": "91", "vak": "B", "geometrieen": [], } @@ -667,9 +668,9 @@ { "id": 288, "uuid": "230727a9-aabf-482d-846f-2dc2128a87a4", - "aangemaakt_op": "2017-06-28T05:03:08 -02:00", - "aangepast_op": "2019-11-25T02:37:45 -01:00", - "origineel_aangemaakt": "2016-12-12T10:07:15 -01:00", + "aangemaakt_op": "2017-06-28T05:03:08", + "aangepast_op": "2019-11-25T02:37:45", + "origineel_aangemaakt": "2016-12-12T10:07:15", "tekst": "Qui labore sint dolor nostrud non nulla in reprehenderit. Eiusmod ut voluptate nostrud dolor. Id fugiat do commodo laboris nulla do ullamco pariatur id eiusmod laborum mollit excepteur ad. Do commodo sunt exercitation Lorem ea ipsum est veniam esse.\r\n", "meta": { "naam_overledene": "Pearson Barnett", @@ -690,7 +691,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Schiebroek", - "grafnummer": "'133'", + "grafnummer": "133", "vak": "E", "geometrieen": [], } @@ -711,9 +712,9 @@ { "id": 65, "uuid": "a2949ed8-d3c8-4b49-b4b5-bbb6628d8341", - "aangemaakt_op": "2018-07-02T02:52:03 -02:00", - "aangepast_op": "2017-12-24T03:20:33 -01:00", - "origineel_aangemaakt": "2018-03-14T05:18:47 -01:00", + "aangemaakt_op": "2018-07-02T02:52:03", + "aangepast_op": "2017-12-24T03:20:33", + "origineel_aangemaakt": "2018-03-14T05:18:47", "tekst": "Nisi magna excepteur cupidatat ut pariatur deserunt excepteur irure aliquip sint. Aliquip culpa deserunt enim labore do veniam nisi qui. Anim ea do ipsum pariatur consectetur dolore consequat consequat eu non culpa excepteur enim minim. Nisi in mollit irure eu duis adipisicing labore ut non nostrud eiusmod ut proident. Non officia veniam sint laboris eu nostrud. Cillum consectetur consectetur veniam eu exercitation irure in aute anim aute adipisicing aliqua veniam. Amet occaecat amet id consectetur ut aliqua enim ullamco nisi mollit aute sit dolor occaecat.\r\n", "meta": { "naam_overledene": "Stein Hayden", @@ -734,7 +735,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Overschie", - "grafnummer": "'81'", + "grafnummer": "81", "vak": "D", "geometrieen": [], } @@ -750,9 +751,9 @@ { "id": 161, "uuid": "96c7f0b7-cfc8-4e8a-9e8e-bc29fede2acf", - "aangemaakt_op": "2017-12-05T01:06:58 -01:00", - "aangepast_op": "2014-06-16T09:24:34 -02:00", - "origineel_aangemaakt": "2021-09-26T10:12:27 -02:00", + "aangemaakt_op": "2017-12-05T01:06:58", + "aangepast_op": "2014-06-16T09:24:34", + "origineel_aangemaakt": "2021-09-26T10:12:27", "tekst": "Do nisi cupidatat aute nostrud. Aute pariatur labore magna reprehenderit qui. Minim consequat culpa ea eu ea do quis exercitation irure sint proident duis. Incididunt pariatur officia ut dolor ad ex. Pariatur duis tempor consectetur veniam est cillum aliqua esse ipsum.\r\n", "meta": { "naam_overledene": "Sparks Mccall", @@ -773,7 +774,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "De Zuiderbegraafplaats", - "grafnummer": "'125'", + "grafnummer": "125", "vak": "B", "geometrieen": [], } @@ -794,9 +795,9 @@ { "id": 395, "uuid": "351a59d6-f21e-4ba7-8f3c-e771a8506ce6", - "aangemaakt_op": "2018-08-04T12:46:14 -02:00", - "aangepast_op": "2020-04-10T07:47:30 -02:00", - "origineel_aangemaakt": "2015-12-08T08:02:12 -01:00", + "aangemaakt_op": "2018-08-04T12:46:14", + "aangepast_op": "2020-04-10T07:47:30", + "origineel_aangemaakt": "2015-12-08T08:02:12", "tekst": "Minim cillum veniam adipisicing minim irure enim excepteur nulla minim commodo irure. Laboris ex proident sint laboris tempor nisi aliqua consequat voluptate ut. Cillum labore cupidatat ad excepteur.\r\n", "meta": { "naam_overledene": "Solis Chambers", @@ -817,7 +818,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Crooswijk", - "grafnummer": "'278'", + "grafnummer": "278", "vak": "G", "geometrieen": [], } @@ -833,9 +834,9 @@ { "id": 394, "uuid": "0736fed7-33f2-4adb-8253-7c47034e6ee6", - "aangemaakt_op": "2014-04-03T11:08:21 -02:00", - "aangepast_op": "2018-02-10T07:36:13 -01:00", - "origineel_aangemaakt": "2015-06-20T07:51:07 -02:00", + "aangemaakt_op": "2014-04-03T11:08:21", + "aangepast_op": "2018-02-10T07:36:13", + "origineel_aangemaakt": "2015-06-20T07:51:07", "tekst": "Veniam anim culpa veniam proident ut cupidatat. Culpa mollit proident magna deserunt ipsum fugiat. Consectetur ipsum irure ipsum est quis excepteur cillum. Voluptate exercitation velit esse proident exercitation voluptate tempor tempor labore irure aute adipisicing ut. Excepteur culpa et nisi deserunt proident consectetur aute enim incididunt sint eu ad magna. Sint consectetur ad anim ullamco ut.\r\n", "meta": { "naam_overledene": "Bettye Cherry", @@ -856,7 +857,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "De Zuiderbegraafplaats", - "grafnummer": "'110'", + "grafnummer": "110", "vak": "E", "geometrieen": [], } @@ -872,9 +873,9 @@ { "id": 145, "uuid": "42de4a4e-a58b-4948-b0e0-e47c94c8d9aa", - "aangemaakt_op": "2021-03-17T10:32:18 -01:00", - "aangepast_op": "2015-04-07T02:49:03 -02:00", - "origineel_aangemaakt": "2015-02-01T10:00:14 -01:00", + "aangemaakt_op": "2021-03-17T10:32:18", + "aangepast_op": "2015-04-07T02:49:03", + "origineel_aangemaakt": "2015-02-01T10:00:14", "tekst": "Occaecat nisi nisi tempor enim ipsum elit id pariatur. Nulla nisi aute nulla ea irure esse officia quis exercitation consequat. Do sunt et sunt deserunt qui.\r\n", "meta": { "naam_overledene": "Wilson Bird", @@ -895,7 +896,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Hoek van Holland", - "grafnummer": "'204'", + "grafnummer": "204", "vak": "C", "geometrieen": [], } @@ -911,9 +912,9 @@ { "id": 473, "uuid": "fb2626fb-5aee-49c8-96eb-8d7621151530", - "aangemaakt_op": "2016-08-02T11:12:55 -02:00", - "aangepast_op": "2014-07-03T12:51:45 -02:00", - "origineel_aangemaakt": "2021-05-17T03:19:15 -02:00", + "aangemaakt_op": "2016-08-02T11:12:55", + "aangepast_op": "2014-07-03T12:51:45", + "origineel_aangemaakt": "2021-05-17T03:19:15", "tekst": "Fugiat irure consequat consequat eiusmod. Adipisicing cupidatat esse incididunt dolor voluptate ex laboris. Irure fugiat ullamco laboris magna sunt id nostrud et do ut. Aute officia minim deserunt fugiat culpa et sit officia elit labore excepteur.\r\n", "meta": { "naam_overledene": "Weaver Adams", @@ -934,7 +935,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Rozenburg", - "grafnummer": "'25'", + "grafnummer": "25", "vak": "G", "geometrieen": [], } @@ -965,9 +966,9 @@ { "id": 290, "uuid": "4372ce36-1e01-463f-85db-23ca04a31ce8", - "aangemaakt_op": "2020-04-16T01:53:20 -02:00", - "aangepast_op": "2017-09-03T03:16:23 -02:00", - "origineel_aangemaakt": "2016-10-01T07:55:41 -02:00", + "aangemaakt_op": "2020-04-16T01:53:20", + "aangepast_op": "2017-09-03T03:16:23", + "origineel_aangemaakt": "2016-10-01T07:55:41", "tekst": "Officia excepteur ex cupidatat aliqua esse. Magna aute ullamco sint incididunt exercitation. Labore nulla deserunt incididunt ex nostrud excepteur. Est amet commodo mollit incididunt.\r\n", "meta": { "naam_overledene": "Glenda England", @@ -988,7 +989,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats en crematorium Hofwijk", - "grafnummer": "'142'", + "grafnummer": "142", "vak": "A", "geometrieen": [], } @@ -1014,9 +1015,9 @@ { "id": 318, "uuid": "5f652aa9-01a6-49e0-9428-f6409aac8ad9", - "aangemaakt_op": "2021-03-04T08:24:39 -01:00", - "aangepast_op": "2016-08-09T09:06:01 -02:00", - "origineel_aangemaakt": "2014-05-05T09:26:17 -02:00", + "aangemaakt_op": "2021-03-04T08:24:39", + "aangepast_op": "2016-08-09T09:06:01", + "origineel_aangemaakt": "2014-05-05T09:26:17", "tekst": "Nisi qui voluptate eiusmod enim dolore ipsum culpa sit. Fugiat voluptate velit aliqua quis officia cupidatat ea do ex dolore duis adipisicing occaecat nostrud. Cupidatat dolor anim ex amet tempor. Sit nisi irure ea ipsum qui occaecat ipsum cupidatat. Minim ea do deserunt irure et qui nostrud proident velit aliqua elit nostrud.\r\n", "meta": { "naam_overledene": "Morton Wiggins", @@ -1037,7 +1038,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats en crematorium Hofwijk", - "grafnummer": "'116'", + "grafnummer": "116", "vak": "F", "geometrieen": [], } @@ -1058,9 +1059,9 @@ { "id": 161, "uuid": "ab1709f0-b559-458b-9f56-b6757486ae4c", - "aangemaakt_op": "2019-07-21T10:54:54 -02:00", - "aangepast_op": "2021-12-09T01:56:27 -01:00", - "origineel_aangemaakt": "2021-06-10T08:39:51 -02:00", + "aangemaakt_op": "2019-07-21T10:54:54", + "aangepast_op": "2021-12-09T01:56:27", + "origineel_aangemaakt": "2021-06-10T08:39:51", "tekst": "Ex proident aute aute magna in quis in proident exercitation aliqua ex dolor deserunt. Incididunt labore Lorem id deserunt in irure velit ut deserunt ullamco. Sunt labore pariatur nulla minim aliqua anim nostrud. Exercitation incididunt in cupidatat id eu. Tempor labore tempor eu ea irure. Aliqua anim cillum pariatur voluptate magna consectetur cillum ut dolore exercitation.\r\n", "meta": { "naam_overledene": "Juliet Peters", @@ -1081,7 +1082,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Overschie", - "grafnummer": "'203'", + "grafnummer": "203", "vak": "G", "geometrieen": [], } @@ -1102,9 +1103,9 @@ { "id": 283, "uuid": "6b56323e-48a8-4430-b527-5c4da27fc76b", - "aangemaakt_op": "2016-07-26T01:28:04 -02:00", - "aangepast_op": "2015-11-04T03:46:18 -01:00", - "origineel_aangemaakt": "2017-12-21T03:54:18 -01:00", + "aangemaakt_op": "2016-07-26T01:28:04", + "aangepast_op": "2015-11-04T03:46:18", + "origineel_aangemaakt": "2017-12-21T03:54:18", "tekst": "Cupidatat magna sit duis sunt in velit laboris enim dolor. In proident eiusmod nostrud adipisicing elit labore magna culpa incididunt ad cillum proident dolor elit. Culpa do nulla ut anim eu commodo eiusmod sint occaecat nisi. Lorem ad irure sit mollit est sunt ut exercitation dolore non. Est ea aliquip enim id eiusmod occaecat. Mollit ad duis deserunt veniam velit culpa aliquip excepteur cupidatat ea consectetur nostrud excepteur.\r\n", "meta": { "naam_overledene": "Leola Francis", @@ -1125,7 +1126,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Overschie", - "grafnummer": "'289'", + "grafnummer": "289", "vak": "B", "geometrieen": [], } @@ -1146,9 +1147,9 @@ { "id": 391, "uuid": "4e38f247-594d-4f7f-a177-a7bbfaac17b3", - "aangemaakt_op": "2017-04-06T07:53:41 -02:00", - "aangepast_op": "2020-02-12T05:45:01 -01:00", - "origineel_aangemaakt": "2016-05-21T08:47:08 -02:00", + "aangemaakt_op": "2017-04-06T07:53:41", + "aangepast_op": "2020-02-12T05:45:01", + "origineel_aangemaakt": "2016-05-21T08:47:08", "tekst": "Nulla nisi non laborum cillum. Velit ex cupidatat id amet reprehenderit reprehenderit ut eu in. Veniam reprehenderit aliqua dolor do tempor non ullamco qui. Do ut laboris aute laborum qui ex cupidatat sit nulla Lorem ad aute labore dolor.\r\n", "meta": { "naam_overledene": "Bettie Mercer", @@ -1169,7 +1170,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats en crematorium Hofwijk", - "grafnummer": "'137'", + "grafnummer": "137", "vak": "E", "geometrieen": [], } @@ -1190,9 +1191,9 @@ { "id": 109, "uuid": "71bffefd-24e1-4f81-bd22-813c435f9a3f", - "aangemaakt_op": "2014-06-30T05:02:35 -02:00", - "aangepast_op": "2019-08-25T06:41:57 -02:00", - "origineel_aangemaakt": "2014-06-26T09:32:03 -02:00", + "aangemaakt_op": "2014-06-30T05:02:35", + "aangepast_op": "2019-08-25T06:41:57", + "origineel_aangemaakt": "2014-06-26T09:32:03", "tekst": "Ea proident Lorem sint pariatur exercitation veniam duis. Eiusmod tempor irure labore Lorem cillum anim Lorem culpa duis anim. Pariatur minim dolore cupidatat ullamco consequat cupidatat ad cupidatat nisi elit non consectetur ex id. Voluptate sit sint duis anim nostrud aute. Minim eu dolore elit officia cillum officia irure. Mollit fugiat minim incididunt sint duis cupidatat aliquip.\r\n", "meta": { "naam_overledene": "Cathy Spence", @@ -1213,7 +1214,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Overschie", - "grafnummer": "'109'", + "grafnummer": "109", "vak": "G", "geometrieen": [], } @@ -1234,9 +1235,9 @@ { "id": 349, "uuid": "bf422202-186b-48e7-84e1-4b9d5c74e61a", - "aangemaakt_op": "2014-08-20T12:49:32 -02:00", - "aangepast_op": "2016-03-20T02:58:02 -01:00", - "origineel_aangemaakt": "2015-05-26T02:31:09 -02:00", + "aangemaakt_op": "2014-08-20T12:49:32", + "aangepast_op": "2016-03-20T02:58:02", + "origineel_aangemaakt": "2015-05-26T02:31:09", "tekst": "Irure commodo sit quis nisi dolor in ullamco est eu incididunt proident. Dolor est reprehenderit cillum do aliquip amet velit laborum ipsum. Pariatur exercitation mollit ad eiusmod qui deserunt occaecat esse ea. Aliquip consectetur commodo enim ad proident aute anim. Tempor minim deserunt labore cillum officia magna mollit irure proident et voluptate do. Non tempor nisi anim laborum nostrud adipisicing eiusmod consectetur in nulla incididunt ut velit duis.\r\n", "meta": { "naam_overledene": "Faye Emerson", @@ -1257,7 +1258,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Hoogvliet", - "grafnummer": "'39'", + "grafnummer": "39", "vak": "D", "geometrieen": [], } @@ -1278,9 +1279,9 @@ { "id": 452, "uuid": "e17d8758-8192-4d1d-ab70-4bae9a1b7b21", - "aangemaakt_op": "2021-03-10T06:40:59 -01:00", - "aangepast_op": "2014-07-17T04:11:04 -02:00", - "origineel_aangemaakt": "2018-06-24T06:14:11 -02:00", + "aangemaakt_op": "2021-03-10T06:40:59", + "aangepast_op": "2014-07-17T04:11:04", + "origineel_aangemaakt": "2018-06-24T06:14:11", "tekst": "Id cupidatat enim do nulla pariatur eiusmod ex velit minim cupidatat exercitation nulla sunt occaecat. Deserunt dolore mollit tempor est ipsum enim veniam sunt adipisicing. Proident sint eu nostrud cupidatat consequat ex irure fugiat ea ipsum aliqua nulla ad. Commodo non culpa sint sint elit est. Commodo magna est anim eiusmod deserunt ad sint adipisicing ullamco anim. Magna minim cillum excepteur amet officia ullamco excepteur excepteur ullamco mollit.\r\n", "meta": { "naam_overledene": "Lee Levy", @@ -1301,7 +1302,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Schiebroek", - "grafnummer": "'86'", + "grafnummer": "86", "vak": "C", "geometrieen": [], } @@ -1322,9 +1323,9 @@ { "id": 411, "uuid": "18e8b3a2-b24f-4e6d-8940-5de1f57406ec", - "aangemaakt_op": "2017-09-22T09:14:44 -02:00", - "aangepast_op": "2015-08-12T07:03:13 -02:00", - "origineel_aangemaakt": "2019-03-26T05:13:33 -01:00", + "aangemaakt_op": "2017-09-22T09:14:44", + "aangepast_op": "2015-08-12T07:03:13", + "origineel_aangemaakt": "2019-03-26T05:13:33", "tekst": "Quis Lorem excepteur enim anim sint ex elit consequat amet et. Aliquip voluptate ea id voluptate aliquip cupidatat in. Enim tempor pariatur proident exercitation anim nostrud officia ea. Ut sint nulla do adipisicing tempor.\r\n", "meta": { "naam_overledene": "Holmes Lindsay", @@ -1345,7 +1346,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Overschie", - "grafnummer": "'196'", + "grafnummer": "196", "vak": "B", "geometrieen": [], } @@ -1361,9 +1362,9 @@ { "id": 369, "uuid": "f19d17f7-a8eb-4066-97f7-7ba60d0a36e9", - "aangemaakt_op": "2019-06-24T07:44:26 -02:00", - "aangepast_op": "2022-07-16T12:10:50 -02:00", - "origineel_aangemaakt": "2018-05-02T10:59:07 -02:00", + "aangemaakt_op": "2019-06-24T07:44:26", + "aangepast_op": "2022-07-16T12:10:50", + "origineel_aangemaakt": "2018-05-02T10:59:07", "tekst": "Nostrud excepteur ipsum culpa deserunt proident incididunt aute aute eiusmod. Exercitation amet minim consequat consequat minim tempor aliqua enim labore mollit dolor sint non fugiat. Sunt qui et laborum eu culpa voluptate sint aute fugiat nulla anim aliqua. Ut nostrud voluptate id veniam cillum sunt veniam voluptate deserunt voluptate irure cillum commodo ut.\r\n", "meta": { "naam_overledene": "Bradley Larsen", @@ -1384,7 +1385,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "De Zuiderbegraafplaats", - "grafnummer": "'276'", + "grafnummer": "276", "vak": "G", "geometrieen": [], } @@ -1405,9 +1406,9 @@ { "id": 267, "uuid": "59ded033-8ffa-4ca1-9910-efc069bedfd1", - "aangemaakt_op": "2023-01-16T08:50:15 -01:00", - "aangepast_op": "2023-01-14T07:02:36 -01:00", - "origineel_aangemaakt": "2017-02-27T09:40:51 -01:00", + "aangemaakt_op": "2023-01-16T08:50:15", + "aangepast_op": "2023-01-14T07:02:36", + "origineel_aangemaakt": "2017-02-27T09:40:51", "tekst": "Magna elit aliqua cupidatat labore culpa quis id. Amet aliqua culpa consequat officia deserunt cupidatat est cupidatat consectetur id. Anim minim magna nostrud magna nulla consectetur amet mollit. Do veniam cupidatat eu elit aliquip ex Lorem consequat minim. Proident sunt elit sint laboris deserunt sit esse velit fugiat nostrud. Incididunt eu aliqua amet anim reprehenderit sint sit elit culpa esse.\r\n", "meta": { "naam_overledene": "Lawson Rogers", @@ -1428,7 +1429,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Rozenburg", - "grafnummer": "'174'", + "grafnummer": "174", "vak": "E", "geometrieen": [], } @@ -1444,9 +1445,9 @@ { "id": 76, "uuid": "d4b8e517-a022-4ed7-85ef-9755938d3ca0", - "aangemaakt_op": "2015-09-23T03:11:21 -02:00", - "aangepast_op": "2020-11-01T05:46:10 -01:00", - "origineel_aangemaakt": "2019-06-08T01:04:20 -02:00", + "aangemaakt_op": "2015-09-23T03:11:21", + "aangepast_op": "2020-11-01T05:46:10", + "origineel_aangemaakt": "2019-06-08T01:04:20", "tekst": "Quis cillum labore dolor culpa commodo voluptate esse cillum culpa anim laboris sit qui. Proident ad occaecat aliqua eiusmod amet in labore nulla eu aliqua. Aliquip eu laborum amet adipisicing Lorem eiusmod esse laborum ex nisi excepteur aliquip.\r\n", "meta": { "naam_overledene": "Alison Meadows", @@ -1467,7 +1468,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats en crematorium Hofwijk", - "grafnummer": "'65'", + "grafnummer": "65", "vak": "F", "geometrieen": [], } @@ -1483,9 +1484,9 @@ { "id": 362, "uuid": "247daca1-6d2b-4a00-9fc9-71911607d75d", - "aangemaakt_op": "2021-11-06T04:15:21 -01:00", - "aangepast_op": "2017-05-13T07:38:44 -02:00", - "origineel_aangemaakt": "2020-11-08T02:39:07 -01:00", + "aangemaakt_op": "2021-11-06T04:15:21", + "aangepast_op": "2017-05-13T07:38:44", + "origineel_aangemaakt": "2020-11-08T02:39:07", "tekst": "Do ullamco in esse ullamco nisi. Mollit est reprehenderit aliquip sit commodo fugiat cillum nisi officia veniam id laborum. Exercitation mollit tempor fugiat Lorem minim tempor. Proident consequat sint duis mollit exercitation elit fugiat incididunt commodo incididunt. Veniam consequat est eu eu laborum proident exercitation sint Lorem duis eiusmod ea anim irure. Ipsum cupidatat do et magna labore.\r\n", "meta": { "naam_overledene": "Rhodes Moss", @@ -1506,7 +1507,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Hoogvliet", - "grafnummer": "'184'", + "grafnummer": "184", "vak": "D", "geometrieen": [], } @@ -1552,9 +1553,9 @@ { "id": 497, "uuid": "0443b92e-4777-41a0-8a63-a110050ccee2", - "aangemaakt_op": "2016-06-14T12:05:24 -02:00", - "aangepast_op": "2020-08-18T05:23:57 -02:00", - "origineel_aangemaakt": "2019-10-09T11:22:37 -02:00", + "aangemaakt_op": "2016-06-14T12:05:24", + "aangepast_op": "2020-08-18T05:23:57", + "origineel_aangemaakt": "2019-10-09T11:22:37", "tekst": "Adipisicing excepteur ullamco et ex Lorem nulla aliqua. Exercitation Lorem qui est dolore ullamco pariatur consectetur Lorem ea dolore officia nisi minim amet. Ullamco esse nostrud consequat commodo nulla occaecat Lorem.\r\n", "meta": { "naam_overledene": "Figueroa Gay", @@ -1575,7 +1576,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "De Zuiderbegraafplaats", - "grafnummer": "'80'", + "grafnummer": "80", "vak": "E", "geometrieen": [], } @@ -1591,9 +1592,9 @@ { "id": 133, "uuid": "7df26ac1-ae81-4361-8496-c1e199478089", - "aangemaakt_op": "2022-09-28T02:15:57 -02:00", - "aangepast_op": "2023-01-17T04:08:58 -01:00", - "origineel_aangemaakt": "2014-11-26T02:31:38 -01:00", + "aangemaakt_op": "2022-09-28T02:15:57", + "aangepast_op": "2023-01-17T04:08:58", + "origineel_aangemaakt": "2014-11-26T02:31:38", "tekst": "Eiusmod nulla non sunt incididunt sint minim veniam nulla non ullamco. Consectetur adipisicing ut elit consequat consectetur magna aliquip eu nisi ullamco non. Nisi adipisicing fugiat voluptate enim incididunt duis fugiat. Irure do ut sint deserunt ipsum ullamco. Occaecat enim laborum consectetur mollit aliqua ipsum.\r\n", "meta": { "naam_overledene": "Marguerite Matthews", @@ -1614,7 +1615,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Schiebroek", - "grafnummer": "'200'", + "grafnummer": "200", "vak": "A", "geometrieen": [], } @@ -1630,9 +1631,9 @@ { "id": 444, "uuid": "2888b347-d7cd-492c-8892-91cd660e2c7e", - "aangemaakt_op": "2018-07-24T12:18:08 -02:00", - "aangepast_op": "2014-01-18T04:00:08 -01:00", - "origineel_aangemaakt": "2018-05-21T10:11:56 -02:00", + "aangemaakt_op": "2018-07-24T12:18:08", + "aangepast_op": "2014-01-18T04:00:08", + "origineel_aangemaakt": "2018-05-21T10:11:56", "tekst": "In non ullamco nisi veniam nostrud est veniam non aliqua qui tempor. Officia et duis labore deserunt laboris laboris. Aute commodo laborum fugiat ut consectetur Lorem labore. Pariatur fugiat reprehenderit ullamco excepteur. Ad elit Lorem pariatur labore nisi proident Lorem veniam commodo laboris. Cupidatat est magna ipsum non. Dolore occaecat dolor commodo pariatur occaecat adipisicing tempor laboris duis.\r\n", "meta": { "naam_overledene": "Angela Gilliam", @@ -1653,7 +1654,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats en crematorium Hofwijk", - "grafnummer": "'56'", + "grafnummer": "56", "vak": "D", "geometrieen": [], } @@ -1669,9 +1670,9 @@ { "id": 132, "uuid": "925b2128-5061-4fb8-8ae0-7102545249a9", - "aangemaakt_op": "2019-08-16T01:10:36 -02:00", - "aangepast_op": "2015-08-07T05:00:52 -02:00", - "origineel_aangemaakt": "2016-08-11T09:15:48 -02:00", + "aangemaakt_op": "2019-08-16T01:10:36", + "aangepast_op": "2015-08-07T05:00:52", + "origineel_aangemaakt": "2016-08-11T09:15:48", "tekst": "Quis commodo duis veniam ad ut ea cupidatat non ex nisi minim qui. Dolor est adipisicing minim do commodo eu sunt nisi aliquip adipisicing. Aliqua consectetur in eiusmod ullamco dolore est ut ad deserunt elit veniam fugiat. Consectetur elit non aliqua laborum ex nulla anim duis ex laboris aliqua. Cupidatat excepteur qui exercitation dolor aute sint dolore eiusmod. Non ut ipsum ad veniam tempor occaecat in nulla et deserunt. Laboris anim velit ad sint labore ad ullamco minim do consequat in dolore sint veniam.\r\n", "meta": { "naam_overledene": "Jane Foster", @@ -1692,7 +1693,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oudeland, Hoogvliet", - "grafnummer": "'299'", + "grafnummer": "299", "vak": "F", "geometrieen": [], } @@ -1738,9 +1739,9 @@ { "id": 477, "uuid": "0801ee9f-9bfe-4e59-975e-028f9a9179c9", - "aangemaakt_op": "2022-05-07T06:55:15 -02:00", - "aangepast_op": "2018-10-20T07:15:19 -02:00", - "origineel_aangemaakt": "2020-12-24T02:20:04 -01:00", + "aangemaakt_op": "2022-05-07T06:55:15", + "aangepast_op": "2018-10-20T07:15:19", + "origineel_aangemaakt": "2020-12-24T02:20:04", "tekst": "Excepteur cillum aliqua et laborum. Nulla sunt reprehenderit elit labore elit pariatur voluptate aute labore. Ipsum deserunt sit Lorem adipisicing laboris ea et labore incididunt ut et sint qui.\r\n", "meta": { "naam_overledene": "Angelia Leblanc", @@ -1761,7 +1762,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Pernis", - "grafnummer": "'293'", + "grafnummer": "293", "vak": "C", "geometrieen": [], } @@ -1822,9 +1823,9 @@ { "id": 382, "uuid": "c7e70076-e252-41b6-ae7c-0b8b296ec8e6", - "aangemaakt_op": "2022-08-10T02:34:34 -02:00", - "aangepast_op": "2021-06-23T05:00:05 -02:00", - "origineel_aangemaakt": "2015-04-07T08:46:27 -02:00", + "aangemaakt_op": "2022-08-10T02:34:34", + "aangepast_op": "2021-06-23T05:00:05", + "origineel_aangemaakt": "2015-04-07T08:46:27", "tekst": "Laboris elit excepteur exercitation ullamco labore culpa. Incididunt est est ea excepteur eu consectetur nulla sint excepteur sit. Reprehenderit adipisicing excepteur laboris reprehenderit incididunt enim magna. Ea sint aute Lorem fugiat adipisicing ad ea voluptate aute. Tempor non sunt velit culpa commodo.\r\n", "meta": { "naam_overledene": "Muriel Perez", @@ -1845,7 +1846,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Rozenburg", - "grafnummer": "'294'", + "grafnummer": "294", "vak": "C", "geometrieen": [], } @@ -1891,9 +1892,9 @@ { "id": 39, "uuid": "3d080670-1ce1-4f38-a2de-930b9c66c112", - "aangemaakt_op": "2022-09-08T04:44:46 -02:00", - "aangepast_op": "2017-11-23T02:23:10 -01:00", - "origineel_aangemaakt": "2019-03-04T05:11:39 -01:00", + "aangemaakt_op": "2022-09-08T04:44:46", + "aangepast_op": "2017-11-23T02:23:10", + "origineel_aangemaakt": "2019-03-04T05:11:39", "tekst": "Consequat quis esse magna irure sint quis nisi amet anim excepteur ad in. Quis nostrud occaecat irure deserunt laborum non magna sint sunt. Irure in occaecat ea aliqua ut incididunt consequat deserunt commodo sint et irure exercitation incididunt. Ea nostrud proident est nisi do. Sit sit laborum deserunt velit culpa proident minim irure dolor duis ad. Officia cillum do ipsum eiusmod deserunt aliqua dolor dolore dolore commodo nulla nostrud deserunt quis.\r\n", "meta": { "naam_overledene": "Rosalind Jacobs", @@ -1914,7 +1915,7 @@ "bron": "geselecteerd_door_gebruiker", "plaatsnaam": "Rotterdam", "begraafplaats": "Begraafplaats Oud-Pernis", - "grafnummer": "'132'", + "grafnummer": "132", "vak": "D", "geometrieen": [], } diff --git a/app/apps/regie/templates/base.html b/app/apps/regie/templates/base.html index 6f6c6525..b6f17f9b 100644 --- a/app/apps/regie/templates/base.html +++ b/app/apps/regie/templates/base.html @@ -5,17 +5,39 @@ {% block title %}Regie{% endblock %} - + {% block stylesheets %} {% render_bundle 'app' 'css' %} {% endblock %} + {% if DEBUG %} + + {% endif %} + + {% block javascripts %} + {% render_bundle 'app' 'js' %} {% endblock %} - {% include 'snippets/pageheader.html' %} + {% block header %}{% include 'snippets/pageheader.html' %}{% endblock %} +
{% block body %}{% endblock %} @@ -23,4 +45,5 @@
+ {% include "auth/check_sso_status.html" %} diff --git a/app/apps/regie/templates/email/email.html b/app/apps/regie/templates/email/email.html index 93c98a6b..37cf852e 100644 --- a/app/apps/regie/templates/email/email.html +++ b/app/apps/regie/templates/email/email.html @@ -209,7 +209,7 @@

Serviceverzoek Begraven & Cremeren

{% if fotos %}{{ fotos }}{% else %}-{% endif %} - Wie heeft het verzoek aangenomen? + Aangenomen door {% if aannemer %}{{ aannemer }}{% else %}-{% endif %} diff --git a/app/apps/regie/templates/email/email.txt b/app/apps/regie/templates/email/email.txt index c53faf16..49471b7e 100644 --- a/app/apps/regie/templates/email/email.txt +++ b/app/apps/regie/templates/email/email.txt @@ -12,7 +12,7 @@ Categorie: {{ categorie }} Omschrijving andere oorzaken: {% if omschrijving_andere_oorzaken %}{{ omschrijving_andere_oorzaken }}{% else %}-{% endif %} Toelichting: {% if toelichting %}{{ toelichting }}{% else %}-{% endif %} Foto's: {% if fotos %}{{ fotos }}{% else %}-{% endif %} -Wie heeft het verzoek aangenomen?: {% if aannemer %}{{ aannemer }}{% else %}-{% endif %} +Aangenomen door: {% if aannemer %}{{ aannemer }}{% else %}-{% endif %} Naam: {% if naam_melder %}{{ naam_melder }}{% else %}-{% endif %} Telefoonnummer: {% if telefoon_melder %}{{ telefoon_melder }}{% else %}-{% endif %} E-mailadres: {% if email_melder %}{{ email_melder }}{% else %}-{% endif %} diff --git a/app/apps/regie/templates/icons/plus.svg b/app/apps/regie/templates/icons/plus.svg index d51b6dd4..14dd8bfd 100644 --- a/app/apps/regie/templates/icons/plus.svg +++ b/app/apps/regie/templates/icons/plus.svg @@ -1,3 +1,3 @@ - + diff --git a/app/apps/regie/templates/melding/index.html b/app/apps/regie/templates/melding/index.html index be09baed..ea3706ae 100644 --- a/app/apps/regie/templates/melding/index.html +++ b/app/apps/regie/templates/melding/index.html @@ -1,45 +1,11 @@ {% extends "base.html" %} {% load is_list from list_tags %} +{% load to_date from date_tags %} + {% block body %} - + + {% endblock%} diff --git a/app/apps/regie/templates/melding/part_detail.html b/app/apps/regie/templates/melding/part_detail.html new file mode 100644 index 00000000..a6ecd5dd --- /dev/null +++ b/app/apps/regie/templates/melding/part_detail.html @@ -0,0 +1,374 @@ +{% extends "base.html" %} +{% load vind_in_dict from querystring_tags %} +{% load to_date from date_tags %} +{% load rotterdam_formulier_html %} +{% load vertaal from vertaal_tags %} + + +{% block body %} + + + +
+
+ + + + + Terug + +
+
+
+
+
+ {% with grafnummer=melding.locaties_voor_melding.0.grafnummer vak=melding.locaties_voor_melding.0.vak plaats=melding.meta.begraafplaats %} +

+ {% if grafnummer %} Grafnummer {{ grafnummer }}{% endif %}{% if vak %}, Vak {{ vak }}{% endif %} + {% if not grafnummer and not vak and plaats %}{% vind_in_dict melding.meta_uitgebreid.begraafplaats.choices plaats %}{% endif %} +

+ {% endwith %} +
+
+
+
+ {% if melding.status %} + {{melding.status.naam|vertaal}}{% if melding.status.naam is 'afgehandeld'%} ({{melding.resolutie}}){% endif %} + {% endif %} + {% if aantal_actieve_taken %} + {{aantal_actieve_taken}} {% if aantal_actieve_taken > 1 %}Taken{%else%}Taak{%endif%} actief + {% endif %} +
+
+
+
+
+
+
+ {% with bijlagen_aantal=melding.bijlagen|length bijlagen_extra_aantal=bijlagen_extra|length %} + {% if melding.bijlagen|length > 0 or bijlagen_extra|length != 0 %} +
+
    + {% for bijlage in melding.bijlagen %} +
  • +
    + Foto van melder +
    +
  • + {% endfor %} + {% for b in bijlagen_extra reversed %} +
  • +
    + Foto toegevoegd door medewerker +
    +
  • + {% endfor %} +
+
+ {% if melding.bijlagen|length > 1 or bijlagen_extra|length != 0 %} +
+
    + {% for bijlage in melding.bijlagen %} +
  • +
    +
    +
    +
  • + {% endfor %} + {% for b in bijlagen_extra reversed %} +
  • +
    +
    +
    +
  • + {% endfor %} +
+
+ {% endif %} + {% else %} +
+ {% endif %} + {% endwith %} +
+
+ {% if melding.bijlagen|length > 0 or bijlagen_extra|length != 0%} + + {% endif %} + +
+
+

+ + + + + + + + + + + + Melding +

+
+
    +
  • +

    Nummer

    +

    + {% if melding.id %} + {{ melding.id }} + {% else %} + - + {% endif %} +

    +
  • +
  • +

    Ingediend

    +

    + {% if melding.origineel_aangemaakt %} + {{ melding.origineel_aangemaakt|to_date|date:"d-m-Y H:i" }} + {% else %} + - + {% endif %} +

    +
  • +
  • +

    Aangenomen door

    +

    + {% if melding.meta.aannemer %} + {{ melding.meta.aannemer }} + {% else %} + - + {% endif %} +

    +
  • +
+
    +
  • +

    Onderwerp

    +

    + {% for onderwerp in melding.onderwerpen %} + {{ onderwerp.naam }}{% if not forloop.last %}, {% endif %} + {% endfor %} +

    +
  • +
  • +

    Toelichting

    +

    + {% if melding.omschrijving %} + {{ melding.omschrijving }} + {% else %} + - + {% endif %} +

    +
  • +
+
+
+
+
+
+

+ + + + Locatie +

+
+
    +
  • +

    Begraafplaats

    +

    + {% if melding.meta.begraafplaats %} + {% vind_in_dict melding.meta_uitgebreid.begraafplaats.choices melding.meta.begraafplaats %} + {% else %} + - + {% endif %} +

    +
  • +
  • +

    Naam overledene

    +

    + {% if melding.meta.naam_overledene %} + {{ melding.meta.naam_overledene }} + {% else %} + - + {% endif %} +

    +
  • +
+
    +
  • +

    Grafnummer

    +

    + {% if melding.locaties_voor_melding.0.grafnummer %} + {{ melding.locaties_voor_melding.0.grafnummer }} + {% else %} + n.v.t. + {% endif %} +

    +
  • +
  • +

    Vak

    +

    + {% if melding.locaties_voor_melding.0.vak %} + {{ melding.locaties_voor_melding.0.vak }} + {% else %} + n.v.t. + {% endif %} +

    +
  • +
+
+
+
+ +
+
+

+ + + + Melder +

+
+ +
    + {% if melding.meta.specifiek_graf == "Ja" %} +
  • +

    Is deze persoon de rechthebbende?

    +

    + {{ melding.meta.rechthebbende }} +

    +
  • + {% endif %} +
  • +

    Is terugkoppeling gewenst?

    +

    + {{ melding.meta.terugkoppeling_gewenst }} +

    +
  • +
+
+ +
+
+ + +
+
+ +
+

+ {% if taaktypes and melding.status.naam != 'afgehandeld' %} + + {% endif %} + {% if not melding.afgesloten_op %} + + {% endif %} +

+

+ + + + + + + Download pdf + +

+ {% comment %}

+ + + + + + Verwijderen + +

{% endcomment %} + + +
+ +
+ + + +
+
+ +
+
+
+ + +
+{% endblock body %} diff --git a/app/apps/regie/templates/melding/part_informatie_toevoegen.html b/app/apps/regie/templates/melding/part_informatie_toevoegen.html new file mode 100644 index 00000000..bf85437c --- /dev/null +++ b/app/apps/regie/templates/melding/part_informatie_toevoegen.html @@ -0,0 +1,56 @@ +{% load rotterdam_formulier_html %} +{% load vind_in_dict from querystring_tags %} +{% load to_date from date_tags %} +{% load json_encode from json_tags %} + + +
+

+ + + + + + Tijdlijn +

+
+
+
+ + Voeg een opmerking of foto toe + +
+ {% csrf_token %} + {{ form.opmerking|render_rotterdam_formulier}} +
+ {{ form.bijlagen_extra|render_rotterdam_formulier}} +
+
+ + +
+
+
+
+ + {% include "melding/part_tijdlijn.html" %} +
+
+
diff --git a/app/apps/regie/templates/melding/part_melding_afhandelen.html b/app/apps/regie/templates/melding/part_melding_afhandelen.html new file mode 100644 index 00000000..4bc8aed4 --- /dev/null +++ b/app/apps/regie/templates/melding/part_melding_afhandelen.html @@ -0,0 +1,87 @@ +{% load rotterdam_formulier_html %} +{% load vind_in_dict from querystring_tags %} +{% load to_date from date_tags %} +{% load json_encode from json_tags %} + + + + diff --git a/app/apps/regie/templates/melding/part_overview_table.html b/app/apps/regie/templates/melding/part_overview_table.html new file mode 100644 index 00000000..e2d716b6 --- /dev/null +++ b/app/apps/regie/templates/melding/part_overview_table.html @@ -0,0 +1,149 @@ +{% extends "base.html" %} +{% load qs_ordenen heeft_orden_oplopend vind_in_dict qs_offset from querystring_tags %} +{% load vertaal from vertaal_tags %} +{% load rotterdam_formulier_html %} + +{% load to_date from date_tags %} + +{% block body %} +
+ + +
+
+

Meldingen

+ {{ startNum }} - {{ endNum }} van {{ totaal }} resultaten +
+ +
+ +
+ +
+ {{ form.begraafplaats|render_rotterdam_formulier }} + {{ form.ordering|render_rotterdam_formulier }} + {{ form.limit|render_rotterdam_formulier }} +
+
+ + + + + Nieuw + +
+ +
+ + + + + + + + + + + + + + + + {% for melding in meldingen %} + + + + + + + + + + + + + + {% endfor %} + +
MeldingLocatieGrafnummerVakCategorieDatumStatus
+ {% if melding.bijlagen.0.afbeelding_verkleind_relative_url %} +
+ {% elif melding.bijlagen.0.bestand_relative_url %} +
+ {% else %} +
+ {% endif %} +
{{melding.id}}{% vind_in_dict filter_options.begraafplaats melding.locaties_voor_melding.0.begraafplaats %}{% if melding.locaties_voor_melding.0.grafnummer %}{{ melding.locaties_voor_melding.0.grafnummer }} {% else %}n.v.t.{% endif %}{% if melding.locaties_voor_melding.0.vak %}{{melding.locaties_voor_melding.0.vak}} {% else %}n.v.t.{% endif %} + {% for onderwerp in melding.onderwerpen %} + {% vind_in_dict filter_options.onderwerp onderwerp %}{% if not forloop.last %}, {% endif %} + {% endfor %} + + {{melding.origineel_aangemaakt|to_date|date:"d-m-Y H:i"}} + + {{ melding.status.naam|vertaal }} + {% if melding.aantal_actieve_taken is not 0 %} ({{ melding.aantal_actieve_taken }}){% endif %} + + + + + + + + + +
+
+ + {% if totaal > meldingen|length %} +
+ +
+ {% endif %} +
+
+ + + +
+{% endblock%} diff --git a/app/apps/regie/templates/melding/part_taak_afronden.html b/app/apps/regie/templates/melding/part_taak_afronden.html new file mode 100644 index 00000000..54043406 --- /dev/null +++ b/app/apps/regie/templates/melding/part_taak_afronden.html @@ -0,0 +1,63 @@ +{% load rotterdam_formulier_html %} +{% load vind_in_dict from querystring_tags %} +{% load to_date from date_tags %} +{% load json_encode from json_tags %} + + + + diff --git a/app/apps/regie/templates/melding/part_taak_starten.html b/app/apps/regie/templates/melding/part_taak_starten.html new file mode 100644 index 00000000..ca6c9861 --- /dev/null +++ b/app/apps/regie/templates/melding/part_taak_starten.html @@ -0,0 +1,56 @@ +{% load rotterdam_formulier_html %} +{% load vind_in_dict from querystring_tags %} +{% load to_date from date_tags %} +{% load json_encode from json_tags %} + + + + diff --git a/app/apps/regie/templates/melding/part_tijdlijn.html b/app/apps/regie/templates/melding/part_tijdlijn.html new file mode 100644 index 00000000..2928033b --- /dev/null +++ b/app/apps/regie/templates/melding/part_tijdlijn.html @@ -0,0 +1,137 @@ +{% load vind_in_dict from querystring_tags %} +{% load to_date from date_tags %} +{% load taakopdracht from melding_tags %} +{% load multiply previous next from list_tags %} +{% load gebruiker_middels_email from gebruikers_tags %} +{% load rotterdam_formulier_html %} + + diff --git a/app/apps/regie/templates/pdf/base.html b/app/apps/regie/templates/pdf/base.html new file mode 100644 index 00000000..b3bfbdec --- /dev/null +++ b/app/apps/regie/templates/pdf/base.html @@ -0,0 +1,17 @@ + + + + + + {% block title %}Regie{% endblock %} + + + {% include 'pdf/pageheader.html' with title="Serviceverzoek" %} +
+
+ {% block body %}{% endblock %} +
+
+
+ + diff --git a/app/apps/regie/templates/pdf/melding.html b/app/apps/regie/templates/pdf/melding.html new file mode 100644 index 00000000..5adb872f --- /dev/null +++ b/app/apps/regie/templates/pdf/melding.html @@ -0,0 +1,9 @@ +{% extends "pdf/base.html" %} + + +{% block body %} + +
+ {% include "melding/part_detail.html" %} +
+{% endblock%} diff --git a/app/apps/regie/templates/pdf/pageheader.html b/app/apps/regie/templates/pdf/pageheader.html new file mode 100644 index 00000000..1cc0d615 --- /dev/null +++ b/app/apps/regie/templates/pdf/pageheader.html @@ -0,0 +1,9 @@ +{% load webpack_static from webpack_loader %} + +
+
+ + Logo organisatie + +
+
diff --git a/app/apps/regie/templates/snippets/pageheader.html b/app/apps/regie/templates/snippets/pageheader.html index 02465004..869c314b 100644 --- a/app/apps/regie/templates/snippets/pageheader.html +++ b/app/apps/regie/templates/snippets/pageheader.html @@ -1,8 +1,167 @@ {% load webpack_static from webpack_loader %} +{% load gebruikersnaam from gebruikers_tags %} + +
+
+ +
+
- - Logo organisatie + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/app/apps/regie/templates/widgets/radio_option.html b/app/apps/regie/templates/widgets/radio_option.html new file mode 100644 index 00000000..61dd24be --- /dev/null +++ b/app/apps/regie/templates/widgets/radio_option.html @@ -0,0 +1,4 @@ + + diff --git a/app/apps/regie/templatetags/__init__.py b/app/apps/regie/templatetags/__init__.py index 4fa3234e..e69de29b 100644 --- a/app/apps/regie/templatetags/__init__.py +++ b/app/apps/regie/templatetags/__init__.py @@ -1,2 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (C) 2018 - 2021 Gemeente Amsterdam diff --git a/app/apps/regie/templatetags/date_tags.py b/app/apps/regie/templatetags/date_tags.py new file mode 100644 index 00000000..e2f59c79 --- /dev/null +++ b/app/apps/regie/templatetags/date_tags.py @@ -0,0 +1,16 @@ +from datetime import datetime + +from django import template + +register = template.Library() + + +@register.filter +def to_date(value): + if not value: + return + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z") + except Exception as e: + print(e) + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S%z") diff --git a/app/apps/regie/templatetags/gebruikers_tags.py b/app/apps/regie/templatetags/gebruikers_tags.py new file mode 100644 index 00000000..fc0ae62c --- /dev/null +++ b/app/apps/regie/templatetags/gebruikers_tags.py @@ -0,0 +1,21 @@ +from django import template +from django.contrib.auth import get_user_model +from utils.diversen import gebruikersnaam as gebruikersnaam_basis + +register = template.Library() + + +@register.filter +def gebruikersnaam(value): + return gebruikersnaam_basis(value) + + +@register.filter +def gebruiker_middels_email(value): + if not value: + return "" + UserModel = get_user_model() + gebruiker = UserModel.objects.filter(email=value).first() + if gebruiker: + return gebruikersnaam_basis(gebruiker) + return value diff --git a/app/apps/regie/templatetags/json_tags.py b/app/apps/regie/templatetags/json_tags.py new file mode 100644 index 00000000..982f9216 --- /dev/null +++ b/app/apps/regie/templatetags/json_tags.py @@ -0,0 +1,10 @@ +import json + +from django import template + +register = template.Library() + + +@register.filter +def json_encode(value): + return json.dumps(value) diff --git a/app/apps/regie/templatetags/list_tags.py b/app/apps/regie/templatetags/list_tags.py index d0c116b6..5d15ca18 100644 --- a/app/apps/regie/templatetags/list_tags.py +++ b/app/apps/regie/templatetags/list_tags.py @@ -7,3 +7,32 @@ def is_list(element): list_types = (list, tuple) return type(element) in list_types + + +@register.filter +def multiply(value_a, value_b): + return value_a * value_b + + +@register.filter +def next(some_list, current_index): + """ + Returns the next element of the list using the current index if it exists. + Otherwise returns an empty string. + """ + try: + return some_list[int(current_index) + 1] # access the next element + except Exception: + return "" # return empty string in case of exception + + +@register.filter +def previous(some_list, current_index): + """ + Returns the previous element of the list using the current index if it exists. + Otherwise returns an empty string. + """ + try: + return some_list[int(current_index) - 1] # access the previous element + except Exception: + return {} # return empty string in case of exception diff --git a/app/apps/regie/templatetags/melding_tags.py b/app/apps/regie/templatetags/melding_tags.py new file mode 100644 index 00000000..59bdc088 --- /dev/null +++ b/app/apps/regie/templatetags/melding_tags.py @@ -0,0 +1,11 @@ +from django import template + +register = template.Library() + + +@register.filter +def taakopdracht(melding, taakopdracht_id): + taakopdracht = { + to.get("id"): to for to in melding.get("taakopdrachten_voor_melding", []) + }.get(taakopdracht_id, {}) + return taakopdracht diff --git a/app/apps/regie/templatetags/querystring_tags.py b/app/apps/regie/templatetags/querystring_tags.py new file mode 100644 index 00000000..aaccc3ae --- /dev/null +++ b/app/apps/regie/templatetags/querystring_tags.py @@ -0,0 +1,51 @@ +from apps.regie.utils import dict_to_querystring +from django import template + +register = template.Library() + + +@register.filter +def qs_ordenen(request_get, orden_param): + qs_huidige_dict = dict(request_get) + if orden_param in qs_huidige_dict.get("ordering", []): + qs_huidige_dict.update({"ordering": [f"-{orden_param}"]}) + else: + qs_huidige_dict.update({"ordering": [orden_param]}) + + return dict_to_querystring(qs_huidige_dict) + + +@register.filter +def heeft_orden_oplopend(request_get, orden_param): + qs_huidige_dict = dict(request_get) + if f"-{orden_param}" in qs_huidige_dict.get("ordering", []): + + return "sorting--down" + + if orden_param in qs_huidige_dict.get("ordering", []): + + return "sorting--up" + + # show sorting icon on default + if len(qs_huidige_dict) == 0 and orden_param == "origineel_aangemaakt": + return "sorting--down" + + return "" + + +@register.filter +def qs_offset(request_get, offset_param): + qs_huidige_dict = dict(request_get) + qs_huidige_dict.update({"offset": [offset_param]}) + + return dict_to_querystring(qs_huidige_dict) + + +@register.simple_tag +def vind_in_dict(op_zoek_dict, key): + if type(op_zoek_dict) != dict: + return key + result = op_zoek_dict.get(key, op_zoek_dict.get(str(key), key)) + if isinstance(result, (list, tuple)): + return result[0] + return result diff --git a/app/apps/regie/templatetags/vertaal_tags.py b/app/apps/regie/templatetags/vertaal_tags.py new file mode 100644 index 00000000..36c99adc --- /dev/null +++ b/app/apps/regie/templatetags/vertaal_tags.py @@ -0,0 +1,9 @@ +from apps.regie.constanten import VERTALINGEN +from django import template + +register = template.Library() + + +@register.filter +def vertaal(value): + return VERTALINGEN.get(value, value) diff --git a/app/apps/regie/tests/__init__.py b/app/apps/regie/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/apps/regie/tests/tests_utils.py b/app/apps/regie/tests/tests_utils.py new file mode 100644 index 00000000..5a48598e --- /dev/null +++ b/app/apps/regie/tests/tests_utils.py @@ -0,0 +1,13 @@ +from apps.regie.utils import snake_case +from django.test import SimpleTestCase + + +class TestUtils(SimpleTestCase): + def test_snake_case(self): + """ + text_list only when an reopens when unhappy string + """ + string_in = "Mock data mock mock" + string_out = "mock_data_mock_mock" + + self.assertEqual(snake_case(string_in), string_out) diff --git a/app/apps/regie/utils.py b/app/apps/regie/utils.py index 78b314dd..0361030b 100644 --- a/app/apps/regie/utils.py +++ b/app/apps/regie/utils.py @@ -1,9 +1,80 @@ +import base64 from re import sub +from django.core.files.storage import default_storage +from django.http import QueryDict -def snake_case(s): + +def snake_case(s: str) -> str: return "_".join( sub( "([A-Z][a-z]+)", r" \1", sub("([A-Z]+)", r" \1", s.replace("-", " ")) ).split() ).lower() + + +def dict_to_querystring(d: dict) -> str: + return "&".join([f"{p}={v}" for p, l in d.items() for v in l]) + + +def querystring_to_dict(s: str) -> dict: + return dict(QueryDict(s)) + + +def to_base64(file): + binary_file = default_storage.open(file) + binary_file_data = binary_file.read() + base64_encoded_data = base64.b64encode(binary_file_data) + base64_message = base64_encoded_data.decode("utf-8") + return base64_message + + +def melding_naar_tijdlijn(melding: dict): + tijdlijn_data = [] + t_ids = [] + row = [] + for mg in reversed(melding.get("meldinggebeurtenissen", [])): + row = [0 for t in t_ids] + + tg = mg.get("taakgebeurtenis", {}) if mg.get("taakgebeurtenis", {}) else {} + taakstatus_is_voltooid = ( + tg and tg.get("taakstatus", {}).get("naam") == "voltooid" + ) + t_id = tg.get("taakopdracht") + if t_id and t_id not in t_ids: + try: + i = t_ids.index(-1) + t_ids[i] = t_id + row[i] = 1 + except Exception: + t_ids.append(t_id) + row.append(1) + + if taakstatus_is_voltooid: + index = t_ids.index(t_id) + row[index] = 2 + + for index, t in enumerate(t_ids): + row[index] = -1 if t == -1 else row[index] + + row.insert(0, 0 if tg else 1) + + if taakstatus_is_voltooid: + index = t_ids.index(t_id) + if index + 1 >= len(t_ids): + del t_ids[-1] + else: + t_ids[index] = -1 + + row_dict = { + "mg": mg, + "row": row, + } + tijdlijn_data.append(row_dict) + + row_dict = { + "row": [t if t not in [1, 2] else 0 for t in row], + } + tijdlijn_data.append(row_dict) + tijdlijn_data = [t for t in reversed(tijdlijn_data)] + return tijdlijn_data diff --git a/app/apps/regie/views.py b/app/apps/regie/views.py index ada31066..f3f1770d 100644 --- a/app/apps/regie/views.py +++ b/app/apps/regie/views.py @@ -1,7 +1,36 @@ -from apps.regie.mock import meldingen +import copy +import logging +import math + +import requests +import weasyprint +from apps.meldingen import service_instance +from apps.meldingen.utils import get_meldingen_token, get_taaktypes +from apps.regie.forms import ( + BEHANDEL_OPTIES, + BEHANDEL_RESOLUTIE, + BEHANDEL_STATUS, + TAAK_BEHANDEL_OPTIES, + TAAK_BEHANDEL_RESOLUTIE, + TAAK_BEHANDEL_STATUS, + FilterForm, + InformatieToevoegenForm, + MeldingAfhandelenForm, + TaakAfrondenForm, + TaakStartenForm, +) +from apps.regie.utils import melding_naar_tijdlijn, to_base64 +from config.context_processors import general_settings +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.core.files.storage import default_storage +from django.http import HttpResponse, QueryDict, StreamingHttpResponse from django.shortcuts import redirect, render +from django.template.loader import render_to_string from django.urls import reverse +logger = logging.getLogger(__name__) + def http_404(request): return render( @@ -17,15 +46,354 @@ def http_500(request): ) +@login_required +def overview(request): + standaard_waardes = { + "ordering": "-origineel_aangemaakt", + "offset": "0", + "limit": "10", + } + + query_dict = QueryDict("", mutable=True) + query_dict.update(standaard_waardes) + query_dict.update(request.GET) + + request.session["overview_querystring"] = request.GET.urlencode() + + data = service_instance.get_melding_lijst(query_string=query_dict.urlencode()) + + pagina_aantal = math.ceil(data.get("count", 0) / int(query_dict.get("limit"))) + offset_options = [ + (str(p * int(query_dict.get("limit"))), str(p + 1)) + for p in range(0, pagina_aantal) + ] + query_dict["offset"] = ( + query_dict.get("offset") + if str(query_dict.get("offset")) in [str(oo[0]) for oo in offset_options] + else 0 + ) + + locaties_geselecteerd = len(query_dict.getlist("begraafplaats")) + begraafplaatsen = [ + [k, f"{v[0]}"] + for k, v in data.get("filter_options", {}).get("begraafplaats", {}).items() + ] + + form = FilterForm( + query_dict, offset_options=offset_options, locatie_opties=begraafplaatsen + ) + + filter_form_data = copy.deepcopy(standaard_waardes) + if form.is_valid(): + filter_form_data = copy.deepcopy(form.cleaned_data) + limit = int(filter_form_data.get("limit", "10")) + offset = int(filter_form_data.get("offset", "0")) + ordering = filter_form_data.get("ordering") + + meldingen = data.get("results", []) + totaal = data.get("count", 0) + pageNumTotal = int( + (totaal - (totaal % limit)) / limit + (1 if totaal % limit > 0 else 0) + ) + pages = [] + for pageNum in range(pageNumTotal): + pages.append(f"limit={limit}&offset={pageNum * limit}&ordering={ordering}") + currentPage = offset / limit + 1 + volgende = data.get("next") + vorige = data.get("previous") + startNum = int((currentPage - 1) * limit) + endNum = int(min([currentPage * limit, totaal])) + melding_aanmaken_url = settings.MELDING_AANMAKEN_URL + + return render( + request, + "melding/part_overview_table.html", + { + "meldingen": meldingen, + "totaal": totaal, + "volgende": volgende, + "vorige": vorige, + "startNum": startNum, + "endNum": endNum, + "form": form, + "locaties_geselecteerd": locaties_geselecteerd, + "filter_options": data.get("filter_options", {}), + "melding_aanmaken_url": melding_aanmaken_url, + }, + ) + + +@login_required +def detail(request, id): + melding = service_instance.get_melding(id) + taaktypes = get_taaktypes(melding) + melding_bijlagen = [ + [bijlage for bijlage in meldinggebeurtenis.get("bijlagen", [])] + + [ + b + for b in ( + meldinggebeurtenis.get("taakgebeurtenis", {}).get("bijlagen", []) + if meldinggebeurtenis.get("taakgebeurtenis") + else [] + ) + ] + for meldinggebeurtenis in melding.get("meldinggebeurtenissen", []) + ] + bijlagen_flat = [b for bl in melding_bijlagen for b in bl] + form = InformatieToevoegenForm() + overview_querystring = request.session.get("overview_querystring", "") + if request.POST: + form = InformatieToevoegenForm(request.POST) + if form.is_valid(): + bijlagen = request.FILES.getlist("bijlagen", []) + bijlagen_base64 = [] + for f in bijlagen: + file_name = default_storage.save(f.name, f) + bijlagen_base64.append({"bestand": to_base64(file_name)}) + + service_instance.melding_status_aanpassen( + id, + omschrijving_intern=form.cleaned_data.get("omschrijving_intern"), + bijlagen=bijlagen_base64, + gebruiker=request.user.email, + ) + return redirect("detail", id=id) + aantal_actieve_taken = len( + [ + to + for to in melding.get("taakopdrachten_voor_melding", []) + if to.get("status", {}).get("naam") != "voltooid" + ] + ) + + return render( + request, + "melding/part_detail.html", + { + "melding": melding, + "form": form, + "overview_querystring": overview_querystring, + "bijlagen_extra": bijlagen_flat, + "taaktypes": taaktypes, + "aantal_actieve_taken": aantal_actieve_taken, + }, + ) + + +@login_required +def melding_afhandelen(request, id): + melding = service_instance.get_melding(id) + afhandel_reden_opties = [(s, s) for s in melding.get("volgende_statussen", ())] + melding_bijlagen = [ + [ + b + for b in ( + meldinggebeurtenis.get("taakgebeurtenis", {}).get("bijlagen", []) + if meldinggebeurtenis.get("taakgebeurtenis") + else [] + ) + ] + for meldinggebeurtenis in melding.get("meldinggebeurtenissen", []) + ] + bijlagen_flat = [b for bl in melding_bijlagen for b in bl] + form = MeldingAfhandelenForm() + if request.POST: + form = MeldingAfhandelenForm(request.POST) + if form.is_valid(): + bijlagen = request.FILES.getlist("bijlagen", []) + bijlagen_base64 = [] + for f in bijlagen: + file_name = default_storage.save(f.name, f) + bijlagen_base64.append({"bestand": to_base64(file_name)}) + + service_instance.melding_status_aanpassen( + id, + omschrijving_extern=form.cleaned_data.get("omschrijving_extern"), + omschrijving_intern=form.cleaned_data.get("omschrijving_intern"), + bijlagen=bijlagen_base64, + gebruiker=request.user.email, + status="afgehandeld", + resolutie="opgelost", + ) + return redirect("detail", id=id) + + return render( + request, + "melding/part_melding_afhandelen.html", + { + "form": form, + "melding": melding, + "afhandel_reden_opties": afhandel_reden_opties, + "standaard_afhandel_teksten": {bo[0]: bo[2] for bo in BEHANDEL_OPTIES}, + "bijlagen": bijlagen_flat, + }, + ) + + +@login_required +def taak_starten(request, id): + melding = service_instance.get_melding(id) + taaktypes = get_taaktypes(melding) + form = TaakStartenForm(taaktypes=taaktypes) + if request.POST: + form = TaakStartenForm(request.POST, taaktypes=taaktypes) + if form.is_valid(): + data = form.cleaned_data + taaktypes_dict = {tt[0]: tt[1] for tt in taaktypes} + service_instance.taak_aanmaken( + melding_uuid=id, + taaktype_url=data.get("taaktype"), + titel=taaktypes_dict.get(data.get("taaktype"), data.get("taaktype")), + bericht=data.get("bericht"), + gebruiker=request.user.email, + ) + return redirect("detail", id=id) + + return render( + request, + "melding/part_taak_starten.html", + { + "form": form, + "melding": melding, + }, + ) + + +@login_required +def taak_afronden(request, melding_uuid, taakopdracht_uuid): + melding = service_instance.get_melding(melding_uuid) + taakopdrachten = { + to.get("uuid"): to for to in melding.get("taakopdrachten_voor_melding", []) + } + taakopdracht = taakopdrachten.get(str(taakopdracht_uuid), {}) + form = TaakAfrondenForm() + if request.POST: + form = TaakAfrondenForm(request.POST) + if form.is_valid(): + bijlagen = request.FILES.getlist("bijlagen_extra", []) + bijlagen_base64 = [] + for f in bijlagen: + file_name = default_storage.save(f.name, f) + bijlagen_base64.append({"bestand": to_base64(file_name)}) + service_instance.taak_status_aanpassen( + taakopdracht_url=taakopdracht.get("_links", {}).get("self"), + status=TAAK_BEHANDEL_STATUS.get(form.cleaned_data.get("status")), + resolutie=TAAK_BEHANDEL_RESOLUTIE.get(form.cleaned_data.get("status")), + omschrijving_intern=form.cleaned_data.get("omschrijving_intern"), + bijlagen=bijlagen_base64, + gebruiker=request.user.email, + ) + + return redirect("detail", id=melding_uuid) + + return render( + request, + "melding/part_taak_afronden.html", + { + "form": form, + "melding": melding, + "taakopdracht": taakopdracht, + }, + ) + + +@login_required +def informatie_toevoegen(request, id): + melding = service_instance.get_melding(id) + tijdlijn_data = melding_naar_tijdlijn(melding) + form = InformatieToevoegenForm() + if request.POST: + form = InformatieToevoegenForm(request.POST) + if form.is_valid(): + bijlagen = request.FILES.getlist("bijlagen_extra", []) + bijlagen_base64 = [] + for f in bijlagen: + file_name = default_storage.save(f.name, f) + bijlagen_base64.append({"bestand": to_base64(file_name)}) + + service_instance.melding_gebeurtenis_toevoegen( + id, + bijlagen=bijlagen_base64, + omschrijving_intern=form.cleaned_data.get("opmerking"), + gebruiker=request.user.email, + ) + return redirect("detail", id=id) + + return render( + request, + "melding/part_informatie_toevoegen.html", + { + "melding": melding, + "form": form, + "tijdlijn_data": tijdlijn_data, + }, + ) + + def root(request): return redirect(reverse("melding_lijst")) +@login_required def melding_lijst(request): + return render( request, "melding/index.html", { - "meldingen": meldingen, + # "meldingen": alle_meldingen, }, ) + + +@login_required +def melding_pdf_download(request, id): + melding = service_instance.get_melding(id) + base_url = request.build_absolute_uri() + path_to_css_file = ( + "/app/frontend/public/build/app.css" if settings.DEBUG else "/static/app.css" + ) + melding_bijlagen = [ + [bijlage for bijlage in meldinggebeurtenis.get("bijlagen", [])] + + [ + b + for b in ( + meldinggebeurtenis.get("taakgebeurtenis", {}).get("bijlagen", []) + if meldinggebeurtenis.get("taakgebeurtenis") + else [] + ) + ] + for meldinggebeurtenis in melding.get("meldinggebeurtenissen", []) + ] + bijlagen_flat = [b for bl in melding_bijlagen for b in bl] + context = { + "melding": melding, + "bijlagen_extra": bijlagen_flat, + "base_url": f"{request.scheme}://{request.get_host()}", + } + context.update(general_settings(request)) + + html = render_to_string("pdf/melding.html", context=context) + + pdf = weasyprint.HTML(string=html, base_url=base_url).write_pdf( + stylesheets=[path_to_css_file] + ) + pdf_filename = f"serviceverzoek_{id}.pdf" + + return HttpResponse( + pdf, + content_type="application/pdf", + headers={"Content-Disposition": f'attachment;filename="{pdf_filename}"'}, + ) + + +def meldingen_bestand(request): + url = f"{settings.MELDINGEN_URL}{request.path}" + headers = {"Authorization": f"Token {get_meldingen_token()}"} + response = requests.get(url, stream=True, headers=headers) + return StreamingHttpResponse( + response.raw, + content_type=response.headers.get("content-type"), + status=response.status_code, + reason=response.reason, + ) diff --git a/app/apps/rotterdam_formulier_html/templates/rotterdam_formulier_html/field.html b/app/apps/rotterdam_formulier_html/templates/rotterdam_formulier_html/field.html index b2315921..fb2b3df2 100644 --- a/app/apps/rotterdam_formulier_html/templates/rotterdam_formulier_html/field.html +++ b/app/apps/rotterdam_formulier_html/templates/rotterdam_formulier_html/field.html @@ -31,10 +31,10 @@ {% if classes.icon %} {% include 'rotterdam_formulier_html/field_icon.html' %} {% endif %} - {% if field.auto_id %} + {% if field.auto_id and not field.field.widget.attrs.hideLabel %}

{{ field.label }}

{% endif %} -
+
{{ field }}
{% for error in field.errors %} @@ -95,21 +95,23 @@

{{ field.label }}

{% if field.auto_id %} {% endif %} - +
+ - {% for error in field.errors %} - {{ error }} - {% endfor %} + {% for error in field.errors %} + {{ error }} + {% endfor %} - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} + {% if field.help_text %} +

+ {{ field.help_text|safe }} +

+ {% endif %} +
{% else %} {% if classes.icon %} @@ -118,30 +120,65 @@

{{ field.label }}

{% if field.auto_id %} {% endif %} - + {% for choice in field %} + {{ choice.tag }} + {% endfor %} + + + {% for error in field.errors %} + {{ error }} {% endfor %} - - {% for error in field.errors %} - {{ error }} + {% if field.help_text %} +

+ {{ field.help_text|safe }} +

+ {% endif %} +
+ + {% endif %} + + +{% elif field|is_multiple_checkbox_thumb %} +
+ {% if field.auto_id and not field.field.widget.attrs.hideLabel %} +

{{ field.label }}{{field.field.widget.attrs.hideLabel}}

+ {% endif %} +
+ + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% if field.help_text %} +

+ {{ field.help_text|safe }} +

{% endif %} +
{% elif field|is_multiple_checkbox %}
- {% if field.auto_id %} -

{{ field.label }}

+ {% if field.auto_id and not field.field.widget.attrs.hideLabel %} +

{{ field.label }}{{field.field.widget.attrs.hideLabel}}

{% endif %}
+ + {% elif field|is_textarea %}
{% if classes.icon %} @@ -179,26 +218,28 @@

{{ field.label }}

{% if field.auto_id %} {% endif %} - +
+ - {% for error in field.errors %} -

{{ error }}

- {% endfor %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} + {% if field.help_text %} +

+ {{ field.help_text|safe }} +

+ {% endif %} +
{% elif field|is_file %}
{% if field.auto_id %} - + {% endif %}
-
{% endif %} diff --git a/app/apps/rotterdam_formulier_html/templatetags/rotterdam_formulier_html.py b/app/apps/rotterdam_formulier_html/templatetags/rotterdam_formulier_html.py index fd1a666b..63b5e93c 100644 --- a/app/apps/rotterdam_formulier_html/templatetags/rotterdam_formulier_html.py +++ b/app/apps/rotterdam_formulier_html/templatetags/rotterdam_formulier_html.py @@ -1,3 +1,4 @@ +from apps.regie.forms import CheckboxSelectMultipleThumb from django import forms, template from django.forms.fields import DateField, DateTimeField from django.http import QueryDict @@ -120,6 +121,10 @@ def _is_multiple_checkbox_widget(widget): return isinstance(widget, forms.CheckboxSelectMultiple) +def _is_multiple_checkbox_thumbs_widget(widget): + return isinstance(widget, CheckboxSelectMultipleThumb) + + def _is_radio_widget(widget): return isinstance(widget, forms.RadioSelect) @@ -147,6 +152,11 @@ def is_multiple_checkbox(field): return isinstance(field.field.widget, forms.CheckboxSelectMultiple) +@register.filter +def is_multiple_checkbox_thumb(field): + return isinstance(field.field.widget, CheckboxSelectMultipleThumb) + + @register.filter def is_radio(field): return isinstance(field.field.widget, forms.RadioSelect) diff --git a/app/config/context_processors.py b/app/config/context_processors.py index cca600b6..b32d4e4e 100644 --- a/app/config/context_processors.py +++ b/app/config/context_processors.py @@ -1,3 +1,30 @@ +import logging + +import jwt +from django.conf import settings +from django.urls import reverse +from utils.diversen import absolute + +logger = logging.getLogger(__name__) + + def general_settings(context): + oidc_id_token = context.session.get("oidc_id_token") + token_decoded = {} + try: + token_decoded = jwt.decode(oidc_id_token, options={"verify_signature": False}) + except Exception: + logger.error("oidc_id_token is niet valide") - return {} + return { + "MELDINGEN_URL": settings.MELDINGEN_URL, + "DEBUG": settings.DEBUG, + "DEV_SOCKET_PORT": settings.DEV_SOCKET_PORT, + "CHECK_SESSION_IFRAME": settings.CHECK_SESSION_IFRAME, + "GET": context.GET, + "ABSOLUTE_ROOT": absolute(context).get("ABSOLUTE_ROOT"), + "OIDC_RP_CLIENT_ID": settings.OIDC_RP_CLIENT_ID, + "SESSION_STATE": token_decoded.get("session_state"), + "LOGOUT_URL": reverse("oidc_logout"), + "LOGIN_URL": f"{reverse('oidc_authentication_init')}?next={absolute(context).get('FULL_URL')}", + } diff --git a/app/config/settings.py b/app/config/settings.py index fb1d6524..81b527f6 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -1,15 +1,19 @@ import locale +import logging import os import sys from os.path import join +import requests + locale.setlocale(locale.LC_ALL, "nl_NL.UTF-8") +logger = logging.getLogger(__name__) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TRUE_VALUES = [True, "True", "true", "1"] -SECRET_KEY = os.environ.get( - "DJANGO_SECRET_KEY", os.environ.get("SECRET_KEY", os.environ.get("APP_SECRET")) +SECRET_KEY = os.getenv( + "DJANGO_SECRET_KEY", os.getenv("SECRET_KEY", os.getenv("APP_SECRET")) ) ENVIRONMENT = os.getenv("ENVIRONMENT") @@ -25,20 +29,34 @@ LANGUAGE_CODE = "nl-NL" LANGUAGES = [("nl", "Dutch")] -DEFAULT_ALLOWED_HOSTS = ".forzamor.nl,localhost,127.0.0.1" +DEFAULT_ALLOWED_HOSTS = ".forzamor.nl,localhost,127.0.0.1,.mor.local" ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", DEFAULT_ALLOWED_HOSTS).split(",") INSTALLED_APPS = ( + "django.contrib.contenttypes", "django.contrib.staticfiles", + "django.contrib.messages", "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.auth", + "django.contrib.admin", + "django.contrib.gis", + "django.contrib.postgres", "rest_framework", + "rest_framework.authtoken", + "drf_spectacular", "webpack_loader", "corsheaders", + "mozilla_django_oidc", "health_check", "health_check.cache", + "health_check.db", + "health_check.contrib.migrations", # Apps + "apps.health", "apps.rotterdam_formulier_html", "apps.regie", + "apps.authenticatie", ) MIDDLEWARE = ( @@ -50,6 +68,9 @@ "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "mozilla_django_oidc.middleware.SessionRefresh", + "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ) @@ -73,9 +94,13 @@ "usb": [], } -STATICFILES_DIRS = [ - "/app/frontend/public/build/", -] +STATICFILES_DIRS = ( + [ + "/app/frontend/public/build/", + ] + if DEBUG + else [] +) STATIC_URL = "/static/" STATIC_ROOT = os.path.normpath(join(os.path.dirname(BASE_DIR), "static")) @@ -83,6 +108,39 @@ MEDIA_URL = "/media/" MEDIA_ROOT = os.path.normpath(join(os.path.dirname(BASE_DIR), "media")) +# Database settings +DATABASE_NAME = os.getenv("DATABASE_NAME") +DATABASE_USER = os.getenv("DATABASE_USER") +DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD") +DATABASE_HOST = os.getenv("DATABASE_HOST_OVERRIDE") +DATABASE_PORT = os.getenv("DATABASE_PORT_OVERRIDE") + +DEFAULT_DATABASE = { + "ENGINE": "django.contrib.gis.db.backends.postgis", + "NAME": DATABASE_NAME, # noqa: + "USER": DATABASE_USER, # noqa + "PASSWORD": DATABASE_PASSWORD, # noqa + "HOST": DATABASE_HOST, # noqa + "PORT": DATABASE_PORT, # noqa +} + +DATABASES = { + "default": DEFAULT_DATABASE, +} +DATABASES.update( + { + "alternate": DEFAULT_DATABASE, + } + if ENVIRONMENT == "test" + else {} +) +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +AUTH_USER_MODEL = "authenticatie.Gebruiker" + +SITE_ID = 1 +SITE_NAME = os.getenv("SITE_NAME", "Regie") +SITE_DOMAIN = os.getenv("SITE_DOMAIN", "localhost") + WEBPACK_LOADER = { "DEFAULT": { "CACHE": not DEBUG, @@ -94,6 +152,32 @@ else "/app/frontend/public/build/webpack-stats.json", } } +DEV_SOCKET_PORT = os.getenv("DEV_SOCKET_PORT", "9000") + + +# Django REST framework settings +REST_FRAMEWORK = dict( + PAGE_SIZE=5, + UNAUTHENTICATED_USER={}, + UNAUTHENTICATED_TOKEN={}, + DEFAULT_PAGINATION_CLASS="rest_framework.pagination.LimitOffsetPagination", + DEFAULT_FILTER_BACKENDS=("django_filters.rest_framework.DjangoFilterBackend",), + DEFAULT_THROTTLE_RATES={ + "nouser": os.getenv("PUBLIC_THROTTLE_RATE", "60/hour"), + }, + DEFAULT_PARSER_CLASSES=[ + "rest_framework.parsers.JSONParser", + "rest_framework.parsers.FormParser", + "rest_framework.parsers.MultiPartParser", + ], + DEFAULT_SCHEMA_CLASS="drf_spectacular.openapi.AutoSchema", + DEFAULT_VERSIONING_CLASS="rest_framework.versioning.NamespaceVersioning", + DEFAULT_PERMISSION_CLASSES=("rest_framework.permissions.IsAuthenticated",), + DEFAULT_AUTHENTICATION_CLASSES=( + "rest_framework.authentication.TokenAuthentication", + ), +) + # Django security settings SECURE_BROWSER_XSS_FILTER = True @@ -118,10 +202,29 @@ # Settings for Content-Security-Policy header CSP_DEFAULT_SRC = ("'self'",) CSP_FRAME_ANCESTORS = ("'self'",) -CSP_SCRIPT_SRC = ("'self'", "'unsafe-eval'", "unpkg.com") -CSP_IMG_SRC = ("'self'", "blob:", "data:", "unpkg.com", "tile.openstreetmap.org") -CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "unpkg.com") -CSP_CONNECT_SRC = ("'self'", "ws:") +CSP_FRAME_SRC = ( + "'self'", + "iam.forzamor.nl", +) +CSP_SCRIPT_SRC = ( + ("'self'", "'unsafe-eval'", "unpkg.com") + if not DEBUG + else ("'self'", "'unsafe-eval'", "unpkg.com", "'unsafe-inline'") +) +CSP_IMG_SRC = [ + "'self'", + "blob:", + "data:", + "unpkg.com", + "tile.openstreetmap.org", + "placehold.it", + "www.placeholder.com", + "via.placeholder.com", + "mor-core-acc.forzamor.nl", +] + +CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "data:", "unpkg.com") +CSP_CONNECT_SRC = ("'self'",) if not DEBUG else ("'self'", "ws:") TEMPLATES = [ { @@ -130,6 +233,8 @@ "APP_DIRS": True, "OPTIONS": { "context_processors": [ + "django.contrib.messages.context_processors.messages", + "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.request", "config.context_processors.general_settings", @@ -162,6 +267,25 @@ EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "0") in TRUE_VALUES DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@forzamor.nl") +MELDINGEN_URL = os.getenv("MELDINGEN_URL", "https://mor-core-acc.forzamor.nl") +MELDINGEN_API_URL = os.getenv("MELDINGEN_API_URL", f"{MELDINGEN_URL}/api/v1") +MELDINGEN_API_HEALTH_CHECK_URL = os.getenv( + "MELDINGEN_API_HEALTH_CHECK_URL", f"{MELDINGEN_URL}/health/" +) +MELDINGEN_TOKEN_API = os.getenv( + "MELDINGEN_TOKEN_API", f"{MELDINGEN_URL}/api-token-auth/" +) +MELDINGEN_TOKEN_TIMEOUT = 60 * 5 +MELDINGEN_USERNAME = os.getenv("MELDINGEN_USERNAME") +MELDINGEN_PASSWORD = os.getenv("MELDINGEN_PASSWORD") + +MELDING_AANMAKEN_URL = os.getenv( + "MELDING_AANMAKEN_URL", + "https://serviceformulier-acc.benc.forzamor.nl/melding/aanmaken", +) + +LOG_LEVEL = "DEBUG" if DEBUG else "INFO" + LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -172,17 +296,87 @@ }, "handlers": { "console": { - "level": "INFO", + "level": "DEBUG", "class": "logging.StreamHandler", "stream": sys.stdout, "formatter": "verbose", }, + "file": { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": "/app/uwsgi.log", + "formatter": "verbose", + }, }, "loggers": { "": { "handlers": ["console"], - "level": "INFO", + "level": LOG_LEVEL, "propagate": True, }, + "celery": { + "handlers": ["console", "file"], + "level": "INFO", + }, }, } + +OIDC_RP_CLIENT_ID = os.getenv("OIDC_RP_CLIENT_ID") +OIDC_RP_CLIENT_SECRET = os.getenv("OIDC_RP_CLIENT_SECRET") +OIDC_VERIFY_SSL = os.getenv("OIDC_VERIFY_SSL", True) in TRUE_VALUES +OIDC_USE_NONCE = os.getenv("OIDC_USE_NONCE", True) in TRUE_VALUES + +OIDC_REALM = os.getenv("OIDC_REALM") +AUTH_BASE_URL = os.getenv("AUTH_BASE_URL") +OPENID_CONFIG_URI = os.getenv( + "OPENID_CONFIG_URI", + f"{AUTH_BASE_URL}/realms{OIDC_REALM}/.well-known/openid-configuration", +) +OPENID_CONFIG = {} +try: + OPENID_CONFIG = requests.get(OPENID_CONFIG_URI).json() +except requests.exceptions.ConnectionError as e: + logger.error(f"OPENID_CONFIG FOUT, url: {OPENID_CONFIG_URI}, error: {e}") + +OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv( + "OIDC_OP_AUTHORIZATION_ENDPOINT", OPENID_CONFIG.get("authorization_endpoint") +) +OIDC_OP_TOKEN_ENDPOINT = os.getenv( + "OIDC_OP_TOKEN_ENDPOINT", OPENID_CONFIG.get("token_endpoint") +) +OIDC_OP_USER_ENDPOINT = os.getenv( + "OIDC_OP_USER_ENDPOINT", OPENID_CONFIG.get("userinfo_endpoint") +) +OIDC_OP_JWKS_ENDPOINT = os.getenv( + "OIDC_OP_JWKS_ENDPOINT", OPENID_CONFIG.get("jwks_uri") +) +CHECK_SESSION_IFRAME = os.getenv( + "CHECK_SESSION_IFRAME", OPENID_CONFIG.get("check_session_iframe") +) +OIDC_RP_SCOPES = os.getenv( + "OIDC_RP_SCOPES", + " ".join(OPENID_CONFIG.get("scopes_supported", ["openid", "email", "profile"])), +) +OIDC_OP_LOGOUT_ENDPOINT = os.getenv( + "OIDC_OP_LOGOUT_ENDPOINT", + OPENID_CONFIG.get("end_session_endpoint"), +) + +if OIDC_OP_JWKS_ENDPOINT: + OIDC_RP_SIGN_ALGO = "RS256" + +AUTHENTICATION_BACKENDS = [ + "apps.authenticatie.auth.OIDCAuthenticationBackend", + "django.contrib.auth.backends.ModelBackend", +] +OIDC_OP_LOGOUT_URL_METHOD = "apps.authenticatie.views.provider_logout" +ALLOW_LOGOUT_GET_METHOD = True +OIDC_STORE_ID_TOKEN = True +OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = int( + os.getenv("OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS", "300") +) + +LOGIN_REDIRECT_URL = "/" +LOGIN_REDIRECT_URL_FAILURE = "/" +LOGOUT_REDIRECT_URL = "/" +LOGIN_URL = "/oidc/authenticate/" diff --git a/app/config/urls.py b/app/config/urls.py index aa9c1d13..dc4601e8 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -1,14 +1,77 @@ -from apps.regie.views import http_404, http_500, melding_lijst, root +from apps.regie.views import ( + detail, + http_404, + http_500, + informatie_toevoegen, + melding_afhandelen, + melding_lijst, + melding_pdf_download, + meldingen_bestand, + overview, + root, + taak_afronden, + taak_starten, +) from django.conf import settings from django.conf.urls.static import static -from django.urls import path +from django.contrib import admin +from django.urls import include, path, re_path +from django.views.generic import RedirectView +from rest_framework.authtoken import views urlpatterns = [ path("", root, name="root"), + path("api-token-auth/", views.obtain_auth_token), + path( + "admin/login/", + RedirectView.as_view( + url="/oidc/authenticate/?next=/admin/", + permanent=False, + ), + name="admin_login", + ), + path( + "admin/logout/", + RedirectView.as_view( + url="/oidc/logout/?next=/admin/", + permanent=False, + ), + name="admin_logout", + ), + path("oidc/", include("mozilla_django_oidc.urls")), + path("admin/", admin.site.urls), path("melding/", melding_lijst, name="melding_lijst"), + path("health/", include("health_check.urls")), + path("part/melding/", overview, name="overview"), + path( + "part/melding//afhandelen/", + melding_afhandelen, + name="melding_afhandelen", + ), + path( + "part/melding//taakstarten/", + taak_starten, + name="taak_starten", + ), + path( + "part/melding//taak-afronden//", + taak_afronden, + name="taak_afronden", + ), + path( + "part/melding//informatie-toevoegen/", + informatie_toevoegen, + name="informatie_toevoegen", + ), + path("melding/", detail, name="detail"), + path( + "download/melding//pdf/", + melding_pdf_download, + name="melding_pdf_download", + ), + re_path(r"media/", meldingen_bestand, name="meldingen_bestand"), ] - if settings.DEBUG: urlpatterns += [ path("404/", http_404, name="404"), diff --git a/app/deploy/config.ini b/app/deploy/config.ini index 863c580f..51ea0b95 100644 --- a/app/deploy/config.ini +++ b/app/deploy/config.ini @@ -5,6 +5,9 @@ static-index = index.html static-map = /static=/static http = 0.0.0.0:8000 +uid = appuser +gid = appuser + strict = true master = true enable-threads = true diff --git a/app/deploy/docker-entrypoint.development.sh b/app/deploy/docker-entrypoint.development.sh index cb47a055..044bbf3b 100644 --- a/app/deploy/docker-entrypoint.development.sh +++ b/app/deploy/docker-entrypoint.development.sh @@ -3,9 +3,17 @@ set -u # crash on missing env variables set -e # stop on any error set -x -rm -rf /static +rm -rf /static/* -echo Test cache -python manage.py test_cache +# echo "Start with a fresh database" +# export PGPASSWORD=${DATABASE_PASSWORD} +# psql -h ${DATABASE_HOST_OVERRIDE} -p 5432 -d ${DATABASE_NAME} -U ${DATABASE_USER} -c "drop schema public cascade;" +# psql -h ${DATABASE_HOST_OVERRIDE} -p 5432 -d ${DATABASE_NAME} -U ${DATABASE_USER} -c "create schema public;" + +echo Apply migrations +python manage.py migrate --noinput + +echo Create superuser +python manage.py createsuperuser --noinput || true exec python -m debugpy --listen 0.0.0.0:5678 /app/manage.py runserver 0.0.0.0:8000 diff --git a/app/deploy/docker-entrypoint.sh b/app/deploy/docker-entrypoint.sh index fcaa497a..58034c77 100644 --- a/app/deploy/docker-entrypoint.sh +++ b/app/deploy/docker-entrypoint.sh @@ -3,10 +3,18 @@ set -u # crash on missing env variables set -e # stop on any error set -x +# echo "Start with a fresh database" +# export PGPASSWORD=${DATABASE_PASSWORD} +# psql -h ${DATABASE_HOST_OVERRIDE} -p 5432 -d ${DATABASE_NAME} -U ${DATABASE_USER} -c "drop schema public cascade;" +# psql -h ${DATABASE_HOST_OVERRIDE} -p 5432 -d ${DATABASE_NAME} -U ${DATABASE_USER} -c "create schema public;" + +echo Apply migrations +python manage.py migrate --noinput + echo Collecting static files python manage.py collectstatic --no-input -echo Test cache -python manage.py test_cache +echo Create superuser +python manage.py createsuperuser --noinput || true exec uwsgi --ini /app/deploy/config.ini diff --git a/app/frontend/assets/controllers/bijlagen_controller.js b/app/frontend/assets/controllers/bijlagen_controller.js new file mode 100644 index 00000000..faf0537a --- /dev/null +++ b/app/frontend/assets/controllers/bijlagen_controller.js @@ -0,0 +1,160 @@ +import { Controller } from '@hotwired/stimulus'; + +let temp_files = {} +let temp_filesArr = [] +let input = null +export default class extends Controller { + + static targets = ['bijlagenExtra', 'bijlagenAfronden'] + connect() { + //clear the filelist + temp_files = {} + temp_filesArr = [] + } + + removeDuplicates(arr) { + var unique = []; + arr.forEach(element => { + if (!unique.includes(element)) { + unique.push(element); + } + }) + return unique + } + + + showFileInput() { + const inputContainer = document.getElementById('id_bijlagen').parentElement; + inputContainer.classList.remove('hidden'); + } + + removeFile (e) { + + const index = e.params.index; + if(this.hasBijlagenExtraTarget) { + input = this.bijlagenExtraTarget + } + if(this.hasBijlagenAfrondenTarget) { + input = this.bijlagenAfrondenTarget + } + temp_filesArr = [...temp_files] + temp_filesArr.splice(index, 1) + + /** Code from: https://stackoverflow.com/a/47172409/8145428 */ + const dT = new ClipboardEvent('').clipboardData || // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655 + new DataTransfer(); // specs compliant (as of March 2018 only Chrome) + + for (let file of temp_filesArr) { + dT.items.add(file); + } + temp_files = dT.files; + input.files = dT.files; + + this.updateImageDisplay(false); + } + + addFiles(newFiles) { + + if (temp_filesArr.length === 0){ + temp_filesArr = [...newFiles] + }else { + temp_filesArr.push(...newFiles) + } + + const dT = new ClipboardEvent('').clipboardData || // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655 + new DataTransfer(); // specs compliant (as of March 2018 only Chrome) + + for (let file of temp_filesArr) { dT.items.add(file); } + temp_files = dT.files; + } + + updateImageDisplay(adding = true) { + if(this.hasBijlagenExtraTarget) { + input = this.bijlagenExtraTarget + } + if(this.hasBijlagenAfrondenTarget) { + input = this.bijlagenAfrondenTarget + } + const preview = input.nextElementSibling + const newFiles = input.files; //contains only new file(s) + if(adding) {this.addFiles(newFiles)} + + + const fileTypes = [ + "image/apng", + "image/bmp", + "image/heic", + "image/gif", + "image/jpeg", + "image/pjpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/webp", + "image/x-icon" + ]; + + function validFileType(file) { + return fileTypes.includes(file.type); + } + + function returnFileSize(number) { + if (number < 1024) { + return `${number} bytes`; + } else if (number >= 1024 && number < 1048576) { + return `${(number / 1024).toFixed(1)} KB`; + } else if (number >= 1048576) { + return `${(number / 1048576).toFixed(1)} MB`; + } + } + + while(preview.firstChild) { + preview.removeChild(preview.firstChild); + } + if (temp_files.length > 0) { + const list = document.createElement('ul'); + list.classList.add('list-clean') + preview.appendChild(list); + + for (const [index, file] of [...temp_files].entries()) { + const listItem = document.createElement('li'); + const content = document.createElement('span'); + const remove = document.createElement('button'); + const span = document.createElement("span") + span.classList.add('container__image') + + remove.setAttribute('type', "button") + remove.setAttribute('data-action', "bijlagen#removeFile") + remove.setAttribute('data-bijlagen-index-param', index) + remove.classList.add('btn-close') + + if (validFileType(file)) { + content.innerHTML = `${file.name} ${returnFileSize(file.size)}`; + if(file.type !== "image/heic"){ + const image = document.createElement('img'); + image.src = URL.createObjectURL(file); + image.onload = () => { + URL.revokeObjectURL(image.src); + }; + span.appendChild(image); + listItem.appendChild(span) + + } else { + const placeholder = document.createElement("div") + placeholder.classList.add("placeholder") + span.textContent = "Van dit bestandstype kan geen preview getoond worden." + placeholder.appendChild(span) + listItem.appendChild(placeholder) + } + listItem.appendChild(content); + listItem.appendChild(remove); + } else { + content.textContent = `Het bestand "${file.name}" is geen geldig bestandstype. Selecteer alleen bestanden van het type "jpg, jpeg of png"`; + listItem.appendChild(content); + } + + list.appendChild(listItem); + } + } + } +} diff --git a/app/frontend/assets/controllers/datetime_controller.js b/app/frontend/assets/controllers/datetime_controller.js new file mode 100644 index 00000000..7f898cc6 --- /dev/null +++ b/app/frontend/assets/controllers/datetime_controller.js @@ -0,0 +1,20 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + + static values = { + dateObject: String, + } + + static targets = ["timeHoursMinutes"] + + connect() { + const dateObject = new Date(this.data.get("dateObjectValue")) + const minutes = dateObject.getMinutes() < 10 ? `0${dateObject.getMinutes()}` : dateObject.getMinutes(); + const time = `${dateObject.getHours()}:${minutes}` + + if(this.hasTimeHoursMinutesTarget) { + this.timeHoursMinutesTarget.textContent = time + } + } +} diff --git a/app/frontend/assets/controllers/detail_controller.js b/app/frontend/assets/controllers/detail_controller.js new file mode 100644 index 00000000..d0231bc6 --- /dev/null +++ b/app/frontend/assets/controllers/detail_controller.js @@ -0,0 +1,65 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + + static targets = ['selectedImage', 'thumbList', 'imageSliderContainer', 'turboActionModal'] + + initialize() { + if(this.hasThumbListTarget) { + this.thumbListTarget.getElementsByTagName('li')[0].classList.add('selected') + } + } + + openModal(event) { + const modal = document.querySelector('.modal'); + const modalBackdrop = document.querySelector('.modal-backdrop'); + + // NOT WORKING ?? this.turboActionModalTarget.setAttribute("src", event) + + const turboActionModal = document.querySelector('#melding_actie_form') + turboActionModal.setAttribute("src", event.params.action) + + modal.classList.add('show'); + modalBackdrop.classList.add('show'); + document.body.classList.add('show-modal'); + } + + closeModal() { + const modal = document.querySelector('.modal'); + const modalBackdrop = document.querySelector('.modal-backdrop'); + modal.classList.remove('show'); + modalBackdrop.classList.remove('show'); + document.body.classList.remove('show-modal'); + } + + onScrollSlider(e) { + this.highlightThumb(Math.floor(this.imageSliderContainerTarget.scrollLeft / this.imageSliderContainerTarget.offsetWidth)) + } + + selectImage(e) { + console.log("selectImage", e.params.imageIndex) + this.imageSliderContainerTarget.scrollTo({left: (Number(e.params.imageIndex) - 1) * this.imageSliderContainerTarget.offsetWidth, top: 0}) + this.deselectThumbs(e.target.closest('ul')); + e.target.closest('li').classList.add('selected'); + } + + highlightThumb(index) { + this.deselectThumbs(this.thumbListTarget) + this.thumbListTarget.getElementsByTagName('li')[index].classList.add('selected') + } + + deselectThumbs(list) { + for (const item of list.querySelectorAll('li')) { + item.classList.remove('selected'); + } + } + + cancelInformatieToevoegen(e) { + console.log("cancelInformatieToevoegen") + const form = e.target.closest("form") + // form.find(input["type=file"]).value=null + form.reset(); + e.target.closest('details').open = false; + } + +} diff --git a/app/frontend/assets/controllers/filter_controller.js b/app/frontend/assets/controllers/filter_controller.js new file mode 100644 index 00000000..bf398627 --- /dev/null +++ b/app/frontend/assets/controllers/filter_controller.js @@ -0,0 +1,19 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + + connect() { + const inputList = document.getElementsByTagName("input") + for (let i=0; i 0) { + this.externalMessage = this.externalTextTarget.textContent + } + } + + // this.element.dispatchEvent(new CustomEvent("formHandleIsConnected", { + // detail: JSON.parse(this.parentContextValue), + // bubbles: true + // })); + } + + cancelHandle() { + this.element.dispatchEvent(new CustomEvent("cancelHandle", { + detail: JSON.parse(this.parentContextValue), + bubbles: true + })); + } + + setExternalMessage(evt){ + if(this.hasExternalTextTarget) { + this.choice = evt.params.index + this.externalMessage = JSON.parse(this.standaardafhandeltekstenValue)[evt.target.value] + this.externalTextTarget.value = this.externalMessage + } + } + + defaultExternalMessage(){ + if(this.externalMessage.length === 0) return + + this.externalTextTarget.value = this.externalMessage + } + + clearExternalMessage() { + this.externalTextTarget.value = "" + } +} diff --git a/app/frontend/assets/controllers/overview_controller.js b/app/frontend/assets/controllers/overview_controller.js new file mode 100644 index 00000000..73221c80 --- /dev/null +++ b/app/frontend/assets/controllers/overview_controller.js @@ -0,0 +1,37 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + + connect() { + + } + + navigate(e) { + if(!e.target.closest("a")) { + Turbo.visit(e.params.targeturl) + } + } + + navigateNext(e) { + e.target.closest('.pagination').querySelector('[checked]').closest('li').nextElementSibling.querySelector('input').click(); + } + navigatePrevious(e) { + e.target.closest('.pagination').querySelector('[checked]').closest('li').previousElementSibling.querySelector('input').click(); + } + openModal() { + const modal = this.element.querySelector('.modal'); + const modalBackdrop = this.element.querySelector('.modal-backdrop'); + + modal.classList.add('show'); + modalBackdrop.classList.add('show'); + document.body.classList.add('show-modal'); + } + + closeModal() { + const modal = this.element.querySelector('.modal'); + const modalBackdrop = this.element.querySelector('.modal-backdrop'); + modal.classList.remove('show'); + modalBackdrop.classList.remove('show'); + document.body.classList.remove('show-modal'); + } +} diff --git a/app/frontend/assets/controllers/request_controller.js b/app/frontend/assets/controllers/request_controller.js deleted file mode 100644 index 60624eb1..00000000 --- a/app/frontend/assets/controllers/request_controller.js +++ /dev/null @@ -1,125 +0,0 @@ -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - - static targets = ["categoryDescription"] - - connect() { - console.log('request_controller connected') - } - removeDuplicates(arr) { - var unique = []; - arr.forEach(element => { - if (!unique.includes(element)) { - unique.push(element); - } - }) - return unique - } - toggleInputOtherCategory(e) { - // TODO fix with turbo-frame and POST - if(e.target.value === "categorie_andere_oorzaken"){ - this.categoryDescriptionTarget.classList.toggle('hidden') - } - } - onChangeSendForm(e) { - console.log("Send form") - // document.getElementById('requestForm').requestSubmit() - } - - showFileInput() { - const inputContainer = document.getElementById('id_fotos').parentElement; - - inputContainer.classList.remove('hidden'); - const preview = document.getElementById('imagesPreview'); - - - console.log('preview', preview) - } - - removeFile (e) { - const index = e.params.index; - const input = document.getElementById('id_fotos') - const fileListArr = [...input.files] - fileListArr.splice(index, 1) - /** Code from: https://stackoverflow.com/a/47172409/8145428 */ - const dT = new ClipboardEvent('').clipboardData || // Firefox < 62 workaround exploiting https://bugzilla.mozilla.org/show_bug.cgi?id=1422655 - new DataTransfer(); // specs compliant (as of March 2018 only Chrome) - - for (let file of fileListArr) { dT.items.add(file); } - input.files = dT.files; - this.updateImageDisplay(); - - } - - updateImageDisplay() { - const input = document.getElementById('id_fotos') - const preview = document.getElementById('imagesPreview'); - const currentFiles = input.files; - - const fileTypes = [ - "image/apng", - "image/bmp", - "image/gif", - "image/jpeg", - "image/pjpeg", - "image/png", - "image/svg+xml", - "image/tiff", - "image/webp", - "image/x-icon" - ]; - - function validFileType(file) { - return fileTypes.includes(file.type); - } - - function returnFileSize(number) { - if (number < 1024) { - return `${number} bytes`; - } else if (number >= 1024 && number < 1048576) { - return `${(number / 1024).toFixed(1)} KB`; - } else if (number >= 1048576) { - return `${(number / 1048576).toFixed(1)} MB`; - } - } - - while(preview.firstChild) { - preview.removeChild(preview.firstChild); - } - - if (currentFiles.length > 0) { - - const list = document.createElement('ul'); - list.classList.add('list-clean') - preview.appendChild(list); - - for (const [index, file] of [...currentFiles].entries()) { - const listItem = document.createElement('li'); - const content = document.createElement('span'); - const remove = document.createElement('button'); - remove.setAttribute('type', "button") - remove.setAttribute('data-action', "request#removeFile") - remove.setAttribute('data-request-index-param', index) - remove.classList.add('btn-close') - - if (validFileType(file)) { - content.innerHTML = `${file.name} ${returnFileSize(file.size)}`; - const image = document.createElement('img'); - image.src = URL.createObjectURL(file); - image.onload = () => { - URL.revokeObjectURL(image.src); - }; - listItem.appendChild(image); - listItem.appendChild(content); - listItem.appendChild(remove); - } else { - content.textContent = `Het bestand "${file.name}" is geen geldig bestandstype. Selecteer alleen bestanden van het type "jpg, jpeg of png"`; - listItem.appendChild(content); - } - - list.appendChild(listItem); - } - } - } -} diff --git a/app/frontend/assets/icons/arrow-long.svg b/app/frontend/assets/icons/arrow-long.svg index dcc765b5..a96d4c13 100644 --- a/app/frontend/assets/icons/arrow-long.svg +++ b/app/frontend/assets/icons/arrow-long.svg @@ -1,3 +1,3 @@ - - + + diff --git a/app/frontend/assets/icons/dropdown.svg b/app/frontend/assets/icons/dropdown.svg new file mode 100644 index 00000000..a20d0bdc --- /dev/null +++ b/app/frontend/assets/icons/dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/frontend/assets/icons/edit.svg b/app/frontend/assets/icons/edit.svg new file mode 100644 index 00000000..35e6a893 --- /dev/null +++ b/app/frontend/assets/icons/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/frontend/assets/icons/history.svg b/app/frontend/assets/icons/history.svg new file mode 100644 index 00000000..7c22d84f --- /dev/null +++ b/app/frontend/assets/icons/history.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/frontend/assets/icons/logout.svg b/app/frontend/assets/icons/logout.svg new file mode 100644 index 00000000..747c886e --- /dev/null +++ b/app/frontend/assets/icons/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/frontend/assets/icons/person.svg b/app/frontend/assets/icons/person.svg index af8a9d94..badf2ff7 100644 --- a/app/frontend/assets/icons/person.svg +++ b/app/frontend/assets/icons/person.svg @@ -1,3 +1,3 @@ - + diff --git a/app/frontend/assets/icons/plus.svg b/app/frontend/assets/icons/plus.svg index d51b6dd4..64af6177 100644 --- a/app/frontend/assets/icons/plus.svg +++ b/app/frontend/assets/icons/plus.svg @@ -1,3 +1,3 @@ - - + + diff --git a/app/frontend/assets/icons/print.svg b/app/frontend/assets/icons/print.svg new file mode 100644 index 00000000..4c99f4cf --- /dev/null +++ b/app/frontend/assets/icons/print.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/frontend/assets/icons/redo.svg b/app/frontend/assets/icons/redo.svg new file mode 100644 index 00000000..99baac64 --- /dev/null +++ b/app/frontend/assets/icons/redo.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/frontend/assets/images/logo-organisatie.png b/app/frontend/assets/images/logo-organisatie.png new file mode 100644 index 00000000..b5b87c0e Binary files /dev/null and b/app/frontend/assets/images/logo-organisatie.png differ diff --git a/app/frontend/assets/styles/_base.scss b/app/frontend/assets/styles/_base.scss index d1d4f0fc..ce9b493c 100644 --- a/app/frontend/assets/styles/_base.scss +++ b/app/frontend/assets/styles/_base.scss @@ -23,7 +23,7 @@ header { > .container { display: block; - max-width: map-get($container-max-widths , sm ); + max-width: map-get($container-max-widths , xxl ); margin: 0 auto; } } @@ -33,6 +33,35 @@ header { padding: 0 map-get($container-margin, s) 16px; } +main { + margin-bottom: 100px; +} + +.grid-container { + display: grid; + grid-auto-columns: minmax(0, 1fr); + gap: 32px; + + .page__detail & { + @media (min-width: map-get($grid-breakpoints, md)) { + grid-template-columns: 66fr 34fr; + } + + .grid-item { + + &.bottom-left { + display: flex; + flex-direction: column; + justify-content: flex-end; + + >* { + align-self: flex-start; + } + } + } + } +} + // HEADINGS h1, .h1 { font-size: $h1-font-size; @@ -69,6 +98,40 @@ h6, .h6 { } } } +// TABLES +table { + font-size: 16px; + border-collapse: collapse; + width: 100%; + + tr:nth-child(even) { + background-color: $gray-tint01; + } + th, td { + padding: 10px 5px 10px 0; + } + thead { + + th { + text-align: left; + } + } + tbody { + + td { + vertical-align: middle; + + &:first-child { + padding: 0; + } + + p { + padding: 0; + margin: 0; + } + } + } +} a, .link { @@ -85,11 +148,14 @@ a, @include has-icon(); } - &.link--email { - @include has-icon(); - } + &.link--email, &.link--phone { - @include has-icon(); + display: flex; + align-items: center; + + svg { + margin-right: 4px; + } } &.link--next { @@ -98,12 +164,13 @@ a, &.link--back { @include has-icon(); - margin: 0 0 20px 20px; + padding-left: 10px; + font-size: 16px; + font-weight: 700; > svg { left: -20px; - path { - fill: $black; - } + top: 3px; + padding-right: 20px; } } @@ -127,7 +194,12 @@ button { p { &.text--person { - @include has-icon(); + display: flex; + align-items: center; + + svg { + margin-right: 4px; + } } &.text--close { @@ -162,6 +234,14 @@ ul { @include list-clean(); } + &.list-horizontal { + @include list-clean(); + margin: 0; + display: flex; + li:not(:last-child) { + margin-right: 12px; + } + } &.list-flex { @media (min-width: map-get($grid-breakpoints, sm)) { display: flex; @@ -181,18 +261,33 @@ dl { dt { margin: 0 0 .5rem; } + dd { + flex: 1 1 60%; + max-width: 60%; + } dt { - margin-right: .5rem; + flex: 1 1 40%; + box-sizing: border-box; + padding-right: .5rem; } } } +dl { + dt { + font-weight: 700; + } + dd { + margin: 0 0 0.5rem; + font-weight: 400; + } +} + // SECTIONS section { &.section--seperated { padding: 1rem 0 0.75rem; - border-bottom: 1px solid $gray-tint04; &__no-border-bottom { border-bottom: 0; @@ -219,6 +314,7 @@ section { } // HELPER-CLASSES + .foldout { height: auto; overflow: hidden; @@ -241,6 +337,11 @@ section { } } +.help-text { + font-size: 16px; + color: $gray-tint09; +} + .hidden-vertical { // display: none; padding-top: 0; @@ -265,6 +366,18 @@ section { pointer-events: none; } +.nowrap { + white-space: nowrap; +} + +.display--flex { + display: flex; +} +.display--flex--center { + display: flex; + align-items: center; +} + .wrapper-horizontal { display: flex; } @@ -272,3 +385,15 @@ section { .invalid-text { color: $invalid-color !important; } + +.border-green { + border-top: 8px solid $green-tint01; +} + +.bar { + background-color: $gray-tint01; + &--top { + height: 48px; + width: 100%; + } +} diff --git a/app/frontend/assets/styles/_print.scss b/app/frontend/assets/styles/_print.scss new file mode 100644 index 00000000..064af547 --- /dev/null +++ b/app/frontend/assets/styles/_print.scss @@ -0,0 +1,101 @@ +.print-only { + display: none; +} + +@media print { + + .print-only { + display: block; + } + + .page__detail { + .aside, + .link--back, + .badge { + display: none; + } + } + + body { + font-size: 14px; + } + + h1 { + margin-top: 0; + font-size: 20px; + } + + .bar, + .section--imageslider { + display: none; + } + + .container__details { + h2 { + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; + font-size: 16px; + + svg { + margin-right: 0; + } + } + .h5 { + font-size: 14px; + } + + >div { + display: flex; + .h5, + .h5+p { + flex: 1 0 50%; + margin: 0; + line-height: 175%; + } + } + } + + .section--imagelist { + display: block; + ul { + &:after { + content:""; + display:block; + clear:both; + } + } + li { + float: left; + width: 48%; + margin-bottom: 20px; + + &:nth-child(odd) { + margin-right: 20px; + } + img { + width: 100%; + max-width: 100%; + } + .image { + position: relative; + + span { + position: absolute; + bottom: 4px; + left: 0; + display: inline-block; + padding: 2px 8px; + background-color: $blue-light; + color: $white; + font-size: 12px; + } + } + + &::after { + content: ""; + clear: both; + display: table; + } + } + } + } diff --git a/app/frontend/assets/styles/_theme.scss b/app/frontend/assets/styles/_theme.scss index 1e0a901b..277c4b58 100644 --- a/app/frontend/assets/styles/_theme.scss +++ b/app/frontend/assets/styles/_theme.scss @@ -16,6 +16,12 @@ $green-tint01: #00811F; $green-tint02: #006E32; $green-tint03: #006E32; +//badges, beetje vreemde kleuren, nog niet elders tegengekomen +$blue-light: #0079B8; +$blue-dark: #083968; +$greenblue: #00846D; +$red-tint02: #D70D0D; + // magentatinten $magenta-tint01: #C93675; $magenta-tint02: #A12B5E; diff --git a/app/frontend/assets/styles/app.scss b/app/frontend/assets/styles/app.scss index 18a7b149..a883b330 100644 --- a/app/frontend/assets/styles/app.scss +++ b/app/frontend/assets/styles/app.scss @@ -1,7 +1,7 @@ @import "./theme"; @import "./base"; @import "./components/fonts"; -@import "./components/full-page-view"; +@import "./components/tables"; @import "./components/buttons"; @import "./components/pageHeader"; @import "./components/forms"; @@ -11,4 +11,10 @@ @import "./components/filters"; @import "./components/incident-details"; @import "./components/spinner"; -@import "./components/alert" +@import "./components/alert"; +@import "./components/pagination"; +@import "./components/badges"; +@import "./components/details-summary"; +@import "./components/imageSlider"; +@import "./components/navigation"; +@import "./print"; diff --git a/app/frontend/assets/styles/components/_badges.scss b/app/frontend/assets/styles/components/_badges.scss new file mode 100644 index 00000000..b16fbaf8 --- /dev/null +++ b/app/frontend/assets/styles/components/_badges.scss @@ -0,0 +1,30 @@ +.badge { + display: inline-block; + padding: 0 8px; + color: $white; + font-weight: 700; + font-size: 12px; + line-height: 20px; + margin-right: 4px; + + &--lightblue { + background-color: $blue-light; + } + + &--darkblue { + background-color: $blue-dark; + } + + &--green { + background-color: $greenblue; + } + + &--red { + background-color: $red-tint02; + } + + &--yellow { + background-color: $warning; + color: $black; + } +} diff --git a/app/frontend/assets/styles/components/_buttons.scss b/app/frontend/assets/styles/components/_buttons.scss index 40822e94..3bb2837a 100644 --- a/app/frontend/assets/styles/components/_buttons.scss +++ b/app/frontend/assets/styles/components/_buttons.scss @@ -214,9 +214,12 @@ button[class="icon"] { margin: 0; text-decoration: underline; max-width: none; + min-height: 0; + line-height: inherit; color: $primary; font-weight: 300; font-size: inherit; + white-space: nowrap; } diff --git a/app/frontend/assets/styles/components/_details-summary.scss b/app/frontend/assets/styles/components/_details-summary.scss new file mode 100644 index 00000000..3c8076ec --- /dev/null +++ b/app/frontend/assets/styles/components/_details-summary.scss @@ -0,0 +1,65 @@ +details { + &.filter { + &--active { + background-color: $gray-tint01; + border: 0; + margin-bottom: $input-padding-x; + + ul { + margin: 0; + } + } + } + + summary { + padding: 24px 16px 24px 40px; + position: relative; + cursor: pointer; + border-top: 1px solid $gray-tint04; + list-style: none; + &::-webkit-details-marker { + display:none; + } + &::before { + content: url('../icons/arrow-right.svg'); + position: absolute; + top: calc(50% - 10px); + left: 16px; + transform: rotate(90deg); + } + + } + + &[open] { + > summary { + &::before { + transform: rotate(-90deg); + left: 19px; + } + } + } + + &.details-form { + summary { + padding-left: 25px; + color: $green-tint01; + text-decoration: underline; + + &::before { + content: url('../icons/plus.svg'); + top: calc(50% - 12px); + left: -5px; + } + } + + &[open] { + > summary { + display: none; + &::before { + left: 0px; + } + } + } + + } +} diff --git a/app/frontend/assets/styles/components/_filters.scss b/app/frontend/assets/styles/components/_filters.scss index 4c669126..ae085c1c 100644 --- a/app/frontend/assets/styles/components/_filters.scss +++ b/app/frontend/assets/styles/components/_filters.scss @@ -1,288 +1,87 @@ -// @use './../base' as base; +.container__filter { + margin-top: $alert-padding-y; + display: flex; -.count { - color: $white; - background-color: $primary; - font-size: 12px; - border-radius: 50%; - width: 24px; - height: 24px; - padding: 0 !important; - display: inline-block; - text-align: center; - line-height: 24px; - margin: 0 3px; - - button & { - font-size: 10px; - width: 15px; - height: 15px; - line-height: 15px; - color: $black; - background-color: $white; + >label { + line-height: 3.2; + margin-right: $alert-padding-x; } -} -.btn-filter { - &--small { - padding-left: 4px; - padding-right: 4px; - min-width: 77px; + > .btn { + padding: 0 16px 0 10px; + flex: 0; } } -.full-page-view__filters { - - form { - padding-bottom: 8rem; - } +.filter-header { + display: flex; + align-items: baseline; - fieldset { - margin: 0 $input-padding-x; + h1 { + margin: 0 16px 0 0; } +} - .show-filters & { - .full-page-view__main { - // opacity: 1; - transform: translateZ(0); +.container__multiselect { + position: relative; + z-index: 10; + flex: 1; + + .toggle { + background-color: white; + border: 1px solid $gray-tint04; + outline: 0; + padding: 12px $input-padding-x; + font-size: 16px; + + &:after { + content: ""; + display: inline-block; + background-image: url(../icons/dropdown.svg); + background-size: 19px 26px; + width: 16px; + height: 19px; + margin-left: 20px; } } - .full-page-view__footer { - position: fixed; - width: calc(100% - 2*($input-padding-x)); - bottom: 0; - padding: $input-padding-x; - background: white; + .wrapper { + flex: 1; + position: absolute; + top: 100%; left: 0; - - .btn { - width: 100%; - margin-bottom: $input-padding-x; - &:last-child { - margin-bottom: 0; - } - } + height: 0; + overflow: hidden; + background-color: #fff; } - details { - &.filter { - &--active { - background-color: $gray-tint01; - border: 0; - margin-bottom: $input-padding-x; - - ul { - margin: 0; - } - } - } - - summary { - display: flex; - justify-content: space-between; - flex-direction: row; - padding: 24px 16px 24px 40px; - position: relative; - cursor: pointer; - border-top: 1px solid $gray-tint04; - - &::-webkit-details-marker { - display:none; - } - &::before { - content: url('../icons/arrow-right.svg'); - position: absolute; - top: calc(50% - 10px); - left: 16px; - transform: rotate(90deg); - } - + &.show { + .wrapper { + height: 280px; + border: 1px solid $gray-tint04; + overflow-y: scroll; } + } - &.filter:not([open]) { - &:last-of-type { - > summary { - border-bottom: 1px solid $gray-tint04; - } - } - } + ul.list--form-check-input { + @include list-clean; + margin: 0; + display: block; - &[open] { - > summary { - &::before { - transform: rotate(-90deg); - left: 19px; - } - } - } - ul { - @include list-clean(); + li { + flex: 0; + padding: $input-padding-x 0; margin: 0 $input-padding-x; - - &.list-filters--active { - li { - display: inline-block; - margin-right: 8px; - } - } - - .container__check { - margin: 0 -16px; - } - } - - details { - summary { - background-color: $gray-tint01; - margin: 0 -16px; - padding: 24px 32px 24px 80px; - - &::before { - left: 57px; - } - } - - ul { - margin: 0; + border-bottom: 1px solid $gray-tint04; + font-size: 16px; + &:last-of-type { + border: 0; } .form-check { - margin-left: -4px; - } - - &[open] { - > summary { - &::before { - left: 60px; - } - } - } - } - .container__list--header { - display: flex; - justify-content: space-between; - align-items: center; - - > div { - flex: 1; - text-align: right; - } - .btn { - flex: 0 1 25%; - white-space: nowrap; - display: inline; - width: auto; - } - } - - } - - .filter { - &:first-of-type{ - > summary { - border-top: 0; - - } - } - } -} - -.container__list--districts, -.container__list--subjects { - background-color: transparent; - margin: 0 0 16px -8px; - padding: 0 0 16px 48px; - - > *:first-child { - margin-top: 0; - } -} - -details { - - summary { - display: flex; - justify-content: space-between; - flex-direction: column; - padding: 24px 16px 24px 40px; - position: relative; - cursor: pointer; - border-top: 1px solid $gray-tint04; - - &::-webkit-details-marker { - display:none; - } - &::before { - content: url('../icons/arrow-right.svg'); - position: absolute; - top: calc(50% - 10px); - left: 16px; - transform: rotate(90deg); - } - - } - - &.filter:not([open]) { - &:last-of-type { - > summary { - border-bottom: 1px solid $gray-tint04; - } - } - } - - &[open] { - > summary { - &::before { - transform: rotate(-90deg); - left: 19px; - } - } - } - - ul { - @include list-clean(); - margin: 0 $input-padding-x; - - &.list-filters--active { - li { - display: inline-block; - margin-right: 8px; - } - } - - .container__check { - margin: 0 -16px; - } - } - - details { - summary { - background-color: $gray-tint01; - margin: 0 -16px; - padding: 24px 32px 24px 80px; - - &::before { - left: 57px; - } - } - - ul { - margin: 0; - } - - .form-check { - margin-left: -4px; - } - - &[open] { - > summary { - &::before { - left: 60px; - } + margin: 0; } } } - } diff --git a/app/frontend/assets/styles/components/_forms.scss b/app/frontend/assets/styles/components/_forms.scss index d6af56b0..d3c6ecce 100644 --- a/app/frontend/assets/styles/components/_forms.scss +++ b/app/frontend/assets/styles/components/_forms.scss @@ -126,6 +126,17 @@ $form-search-indicator-position: left 10px center !default; background-position: $form-search-indicator-position; } } + +.form-control__with-helptext { + .form-control { + margin-bottom: 0.25em; + } + .helptext { + display: inline-block; + margin-bottom: 1em; + } +} + .form-select { background-image: url("data:image/svg+xml, "); background-repeat: no-repeat; @@ -232,6 +243,9 @@ textarea { input { @include form-check-input(); } + label { + font-weight: 400; + } } .list--form-check-input { @@ -253,6 +267,74 @@ textarea { color: $gray-tint08; } +// start switch +.form-switch { + position: relative; + display: inline-block; + display: flex; + margin: 0; + + .form-switch-label { + display: flex; + align-items: center; + } + + /* Hide default HTML checkbox */ + input { + opacity: 0; + width: 0; + height: 0; + } + + /* The slider */ + .container__slider { + display: inline-block; + position: relative; + width: 44px; + height: 24px; + } + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: $gray-tint08; + -webkit-transition: .4s; + transition: .4s; + flex: 1; + } + + .slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + } + + input:checked + .container__slider .slider { + background-color: $green-tint01; + } + + input:focus + .slider { + box-shadow: 0 0 1px $green-tint01; + } + + input:checked + .container__slider .slider:before { + -webkit-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); + } +} + +// end switch + .file-upload { position: relative; margin-bottom: $alert-padding-y; @@ -288,8 +370,7 @@ textarea { padding-right: 56px; img { - height: 116px; - width: 100%; + max-height: 116px; max-width: 155px; vertical-align: middle; margin-right: $input-padding-x; @@ -314,13 +395,12 @@ textarea { } } -#buttonFile { +.btn-files { display: flex; justify-content: center; align-items: center; } - .form-row { position: relative; @@ -353,6 +433,35 @@ textarea { &.container__button { padding: $alert-padding-y 0; } + + .form--horizontal & { + + display: flex; + flex-direction: row; + margin-top: 1rem; + + >.label, + >label { + flex: 1 1 40%; + margin-top: 0; + } + > div{ + flex: 1 1 60%; + } + + &.btn-row { + justify-content: end; + .btn { + margin-left: 16px; + width: auto; + white-space: nowrap; + } + } + + ul { + margin: 0; + } + } } .wrapper-horizontal { @@ -374,3 +483,36 @@ textarea { } } } + +.form--aside { + margin-top: 22px; + + .btn-files { + display: none !important; + } + .file-upload.hidden { + height: auto; + overflow: visible; + } + + .btn { + margin-right: 16px; + } + + textarea { + margin-bottom: 1rem; + } + + .label--file { + display: inline-block; + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: 0; + pointer-events: none; + } +} diff --git a/app/frontend/assets/styles/components/_full-page-view.scss b/app/frontend/assets/styles/components/_full-page-view.scss deleted file mode 100644 index 770cd20a..00000000 --- a/app/frontend/assets/styles/components/_full-page-view.scss +++ /dev/null @@ -1,56 +0,0 @@ -.full-page-view { - &__main { - // opacity: 0; - background-color: $white; - display: flex; - flex-direction: column; - height: 100%; - left: 0; - overflow: hidden; - position: fixed; - top: 0; - transform: translate3d(100%,0,0); - transition: transform .3s ease-in-out, opacity 0.3s ease-in-out; - width: 100%; - z-index: 11; - padding: 0 map-get($container-margin, md) map-get($container-margin, md); - box-sizing: border-box; - } - - &__header { - display: flex; - flex: 1 0 0; - justify-content: space-between; - position: relative; - min-height: 48px; - height: 48px; - align-items: flex-end; - - h2 { - margin-bottom: 0; - } - - .btn-close { - box-sizing: border-box; - width: 48px; - height: 48px; - right: -(map-get($container-margin, md)); - } - } - - &__scroll-pane { - height: 100%; - overflow-x: hidden; - overflow-y: auto; - padding: $input-padding-x 0; - } - - &__content { - height: 100%; - position: relative; - } - - &__footer { - z-index: 1; - } -} diff --git a/app/frontend/assets/styles/components/_imageSlider.scss b/app/frontend/assets/styles/components/_imageSlider.scss new file mode 100644 index 00000000..6dd9dcf6 --- /dev/null +++ b/app/frontend/assets/styles/components/_imageSlider.scss @@ -0,0 +1,122 @@ +.container__imageslider { + overflow-x: scroll; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + width: 100%; + -webkit-overflow-scrolling: touch; + + .imageslider { + display: flex; + left: 0; + margin-top: 0; + } + + .container__image { + width: 100%; + background-color: $gray-tint02; + scroll-snap-align: center; + display: flex; + + &:focus { + border: 3px solid gold; + } + + .image { + width: 100%; + height: 0; + padding-bottom: 75%; + display: block; + margin: auto; + background-size: contain; + background-position: 50% 50%; + background-repeat: no-repeat; + position: relative; + + span { + position: absolute; + bottom: 0; + left: 0; + display: inline-block; + padding: 2px 8px; + background-color: $blue-light; + color: $white; + font-size: 12px; + } + } + } +} + +.container__thumbs { + padding: 16px $container-padding-x 0; + white-space: nowrap; + overflow-x: scroll; + + ul { + list-style: none; + padding: 0; + margin: 0 -8px; + + li { + display: inline-block; + margin: 0 8px; + cursor: pointer; + + &.selected { + .container__image { + opacity: 1; + outline: 2px solid $primary; + } + } + } + } + + .container__image { + width: calc(66px + 2vw); + height: 77px; + background-color: $gray-tint02; + opacity: .5; + outline: none; + + .image { + width: 100%; + height: 100%; + display: block; + margin: auto; + background-size: contain; + background-position: 50% 50%; + background-repeat: no-repeat; + } + } + + &.no-slides { + padding: 0; + width: 100%; + ul { + margin: 0 -8px; + + li { + margin: 0 8px; + cursor: initial; + } + } + + .container__image { + width: 278px; + height: 188px; + background-color: $gray-tint02; + outline: none; + opacity: 1; + + // .image { + // width: 100%; + // height: 0; + // padding-bottom: 100%; + // display: block; + // margin: auto; + // background-size: cover; + // background-position: 50% 50%; + // background-repeat: no-repeat; + // } + } + } +} diff --git a/app/frontend/assets/styles/components/_incident-details.scss b/app/frontend/assets/styles/components/_incident-details.scss index fcf57968..f8ec47ef 100644 --- a/app/frontend/assets/styles/components/_incident-details.scss +++ b/app/frontend/assets/styles/components/_incident-details.scss @@ -1,3 +1,37 @@ +.page__detail { + padding-top: 2rem; + h1 { + margin-bottom: 0; + + span { + font-size: 16px; + } + } + + .aside { + font-size: 16px; + + .btn { + text-align: left; + width: auto; + display: inline-block; + font-size: 16px; + margin-right: 4px; + } + + .list__mutations { + margin-top: 0; + } + } +} + +.container__actions { + p { + margin-bottom: 0.5rem; + } + p:not(:first-of-type) { + margin-top: 0.5rem; + } +} .container__details { > *:first-child { @@ -8,6 +42,21 @@ } h2 { margin-bottom: 0; + display: flex; + align-items: center; + padding-bottom: 1rem; + border-bottom: 1px solid $gray-tint04; + + svg { + margin-right: 8px; + } + } + h3 { + margin-bottom: 0.25rem; + + &:first-child { + margin-top: 0; + } } p { margin-top: 0; @@ -17,6 +66,11 @@ > *:last-child { margin-bottom: 0; } + + ul { + flex: 1 1 50%; + } + } .no-image { @@ -27,129 +81,6 @@ margin: 0 $container-padding-x; } -.container__imageslider { - overflow-x: scroll; - scroll-snap-type: x mandatory; - scroll-behavior: smooth; - width: 100%; - -webkit-overflow-scrolling: touch; - - .imageslider { - display: flex; - left: 0; - margin-top: 0; - } - - .container__image { - max-height: calc(250px + 10vw); - width: 100%; - background-color: $gray-tint02; - scroll-snap-align: center; - - &:focus { - border: 3px solid gold; - } - - img { - max-width: 100%; - max-height: calc(250px + 10vw); - height: 100%; - display: block; - margin: auto; - } - } -} - -.container__thumbs { - padding: 16px $container-padding-x 0; - white-space: nowrap; - overflow-x: scroll; - - ul { - list-style: none; - padding: 0; - margin: 0 -8px; - - li { - display: inline-block; - margin: 0 8px; - cursor: pointer; - - &.selected { - .container__image { - opacity: 1; - outline: 2px solid $primary; - - } - } - } - } - - .container__image { - height: calc(50px + 2vw); - width: calc(66px + 2vw); - background-color: $gray-tint02; - opacity: .5; - outline: none; - - img { - max-width: 100%; - height: 100%; - display: block; - margin: auto; - } - } -} - -.container__map { - height: 0; - padding-bottom: 100%; - - .map { - height: 0; - padding-bottom: 100%; - - .map__overlay { - color: $white; - font-size: 1.5rem; - font-family: 'Bolder', sans-serif; - text-align: center; - justify-content:center; - display: flex; - align-items: center; - position: absolute; - top: 0; - left: 0; - height: 0; - overflow: hidden; - z-index: 9999; - pointer-events:none; - opacity: 0; - background: rgba(0, 0, 0, 0); - transition: background .25s, opacity .25s; - transition-delay: .25s; - } - - &.swiping { - .map__overlay { - right: 0; - bottom: 0; - padding: $alert-padding-x; - height: auto; - opacity: 1; - background: rgba(0, 0, 0, 0.5); - } - } - } - - .section--seperated & { - margin-bottom: -16px !important; - } - - img { - width: 100%; - }; -} .incident-meta-list { li { @@ -179,3 +110,22 @@ display: inline-block; } } + +.container__details--overview { + box-sizing: content-box; + // padding-top: 16px; + .wrapper { + background-color: $gray-tint02; + padding: $input-padding-x; + display: flex; + font-size: 16px; + + dl { + margin: 0; + flex-basis: 40%; + } + } + select { + margin: 0 $container-padding-x 0 0; + } +} diff --git a/app/frontend/assets/styles/components/_list-mutations.scss b/app/frontend/assets/styles/components/_list-mutations.scss index b391cb97..bdfde425 100644 --- a/app/frontend/assets/styles/components/_list-mutations.scss +++ b/app/frontend/assets/styles/components/_list-mutations.scss @@ -1,12 +1,12 @@ .list__mutations { @include list-clean; - border-top: 1px solid $gray-tint04; >li { margin: 0 calc(-1 * (map-get($container-margin, md))); padding: 0 map-get($container-margin, md); - &:nth-child(even) { - background-color: $gray-tint02; - } + position: relative; + // &:nth-child(even) { + // background-color: $gray-tint02; + // } p { margin: 8px 0 0; @@ -18,28 +18,97 @@ height: 0; } } + } - } + &:first-child { + summary { + &::before { + display: none; + } + } + } + + &:first-child { + + details { + .line { + &.line-melding{ + width: 0; + background-color: transparent; + border-left: 2px dashed $green-tint01; + } + &.line-taak { + width: 0; + background-color: transparent; + border-left: 2px dashed $magenta-tint01; + } + } + } + } + + } + .line { + width: 2px; + height: 100%; + position: absolute; + left: 6px; + top: 23px; + background-color: $gray-tint11; + } details { - position: relative; - &::before { + .line { content: ""; width: 2px; - height: 100%; + height: calc(100% + 20px); position: absolute; left: 6px; top: 23px; background-color: $gray-tint11; + &.line-melding { + background-color: $green-tint01; + } + &.line-taak { + background-color: $magenta-tint01; + } } + .event { + &.event-melding { + background-color: $green-tint01; + } + &.event-taak { + background-color: $magenta-tint01; + } + } + + + position: relative; + summary { - padding: 16px 0 16px 24px; + padding: 12px 18px 12px 74px; border: 0; + font-size: $small-font-size; + text-align: right; &::before { - content: ""; - top: 17px; + // display: none; + right: 0; + left: auto; + top: calc(50% - 8px); + } + .line { + width: 2px; + height: 100%; + position: absolute; + left: 6px; + top: 23px; + background-color: $gray-tint11; + } + .event { + position: absolute; + display: block; + top: 13px; left: 0; width: 14px; height: 14px; @@ -47,17 +116,21 @@ background-color: $gray-tint11; } } - - dl { - margin-top: 0; - margin-bottom: 0; - padding-bottom: 16px; + .content { + position: relative; + padding: 8px; + background-color: $gray-tint02; + margin-bottom: 20px; + margin-left: 5px; + font-size: 16px; + text-align: right; } &[open] { > summary { &::before { - left: 0; + right: 0; + left: auto; } } } @@ -79,4 +152,12 @@ margin-bottom: .5rem; } } + + .container__thumbs { + .container__image { + opacity: 1; + cursor: initial; + } + } + } diff --git a/app/frontend/assets/styles/components/_modal.scss b/app/frontend/assets/styles/components/_modal.scss index a7e66834..f448a6a8 100644 --- a/app/frontend/assets/styles/components/_modal.scss +++ b/app/frontend/assets/styles/components/_modal.scss @@ -7,6 +7,12 @@ opacity: 1; visibility: visible; + &.modal-right { + .modal-content { + right: 0; + } + } + .modal-backdrop { display: block; opacity: 0.5; @@ -33,21 +39,31 @@ body.show-modal--first-filter { height: 100%; opacity: 0; visibility: hidden; - transition: all 0.3s ease; + transition: all 1.3s ease; top: 0; left: 0; overflow-x: hidden; overflow-y: auto; z-index: 2100; + .turboframe-container { + display: flex; + align-items: center; + min-height: 100%; + } + &-dialog { display: flex; align-items: center; min-height: calc(100% - 3rem); position: relative; width: 90%; - max-width: $modal-md; + max-width: map-get($container-max-widths , lg ); margin: 1.5rem auto; + + &--small { + max-width: map-get($container-max-widths , md ); + } } &-content { @@ -56,6 +72,7 @@ body.show-modal--first-filter { display: flex; flex-direction: column; width: 100%; + min-height: 100%; z-index: 20; } diff --git a/app/frontend/assets/styles/components/_navigation.scss b/app/frontend/assets/styles/components/_navigation.scss new file mode 100644 index 00000000..9047940b --- /dev/null +++ b/app/frontend/assets/styles/components/_navigation.scss @@ -0,0 +1,30 @@ +.container__nav--tertiary { + margin: 0 auto; + width: 100%; + max-width: map-get($container-max-widths , xxl ); + display: flex; + justify-content: flex-end; +} + + +.list-nav--tertiary { + @include list-clean(); + margin: 12px 16px 12px 0; + display: flex; + li:not(:last-child) { + margin-right: 20px; + } + a { + color: $gray-tint11; + text-decoration: none; + + svg { + vertical-align: bottom; + } + + &:hover, + &:focus { + color: $green-tint01; + } + } +} diff --git a/app/frontend/assets/styles/components/_pageHeader.scss b/app/frontend/assets/styles/components/_pageHeader.scss index 8aa49b7a..f5ef0011 100644 --- a/app/frontend/assets/styles/components/_pageHeader.scss +++ b/app/frontend/assets/styles/components/_pageHeader.scss @@ -1,8 +1,28 @@ header { - height: 64px; - img { - max-height: 40px; - margin: 10px 0 0 10px; + .container { + display: flex; + flex-direction: column; + margin: 1rem auto 0; + + h1 { + margin: 0; + } + svg { + height: 40px; + width: 220px; + margin: 10px 0 0 10px; + } + + @media (min-width: map-get($grid-breakpoints, md)) { + flex-direction: row; + justify-content: space-between; + h1 { + order: 1; + } + >a { + order: 2; + } + } } } diff --git a/app/frontend/assets/styles/components/_pagination.scss b/app/frontend/assets/styles/components/_pagination.scss new file mode 100644 index 00000000..55a13e96 --- /dev/null +++ b/app/frontend/assets/styles/components/_pagination.scss @@ -0,0 +1,95 @@ +.container__pagination { + display: flex; + justify-content: flex-end; + + .pagination { + display: flex; + + li { + flex: 0; + margin: 0 2px; + } + button { + border: 0; + outline: none; + width: 32px; + height: 32px; + background-color: white; + display: flex; + align-items: center; + justify-content: center; + &:hover { + color: $green-tint01; + background-color: #E0EEE2; + } + + &.btn--next { + + path{ + fill: $gray-tint11; + } + } + &.btn--previous { + transform: rotate(180deg); + svg{ + fill: $gray-tint11; + } + } + } + span { + color: $green-tint01; + background-color: #E0EEE2; + } + } + + .wrapper-horizontal { + ul { + + li { + margin-right: 0; + width: 32px; + height: 32px; + text-align: center; + line-height: 32px; + position: relative; + + label { + font-weight: 400; + font-size: 16px; + display: block; + width: 32px; + } + + input { + width: 0; + height: 0; + position: absolute; + border: 0; + box-shadow: none; + + &::before { + content: ""; + display: block; + position: absolute; + width: 32px; + height: 32px; + background-color: white; + z-index: -1; + left: 0; + top: -3px; + } + &:hover, + &:checked { + border: 0; + box-shadow: none; + &::before { + color: $green-tint01; + background-color: #E0EEE2; + } + } + } + + } + } + } +} diff --git a/app/frontend/assets/styles/components/_spinner.scss b/app/frontend/assets/styles/components/_spinner.scss index af9b426e..06bd1f82 100644 --- a/app/frontend/assets/styles/components/_spinner.scss +++ b/app/frontend/assets/styles/components/_spinner.scss @@ -6,6 +6,7 @@ turbo-frame { position: relative; display: block; + width: 100%; &[busy] { &::before { content: ""; diff --git a/app/frontend/assets/styles/components/_tables.scss b/app/frontend/assets/styles/components/_tables.scss new file mode 100644 index 00000000..eef5c1cc --- /dev/null +++ b/app/frontend/assets/styles/components/_tables.scss @@ -0,0 +1,106 @@ +.container__table { + + @media (max-width: map-get($grid-breakpoints, lg)) { + overflow-x: scroll; + } +} + +.sorting { + cursor: pointer; + white-space: nowrap; + &::after { + content: ""; + display: inline-block; + background-image: url(../icons/dropdown.svg); + background-size: 19px 26px; + width: 16px; + height: 19px; + } + + &.sorting--up { + &::after { + background-image: url(../icons/arrow-long.svg); + background-size: 16px 12px; + width: 16px; + height: 13px; + transform: rotate(90deg); + } + } + + &.sorting--down { + &::after { + background-image: url(../icons/arrow-long.svg); + background-size: 16px 12px; + width: 16px; + height: 13px; + transform: rotate(-90deg); + } + } + +} + +.table--overview { + margin: 10px 0 60px; + position: relative; + border-collapse: separate; + border-spacing: 0; + + .container__thumbnail { + background-color: $gray-tint01; + background-size: cover; + background-position: 50% 50%; + height: 48px; + width: 64px; + margin-bottom: 4px; + + .thumbnail { + height: 48px; + display: block; + margin: 0 auto; + } + } + + + .background-image { + background-color: $gray-tint01; + background-image: url(../icons/noimage.svg); + background-size: 40%; + height: 48px; + width: 64px; + background-repeat: no-repeat; + background-position: center; + } + + tr { + &:hover { + cursor: pointer; + background-color: $gray-tint02; + } + } + + th { + position: sticky; + top: 0; + background: $white; + border-bottom: 2px solid $gray-tint11; + vertical-align: bottom; + + a { + text-decoration: none; + color: inherit; + } + } + + .btn-textlink { + display: flex; + padding: 6px 0; + + svg { + padding: 0 8px; + } + + &:hover { + background-color: $white; + } + } +} diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index dda8f351..ca40d0cf 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -29,7 +29,8 @@ "webpack-bundle-tracker": "^1.7.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.11.1", - "webpack-notifier": "^1.15.0" + "webpack-notifier": "^1.15.0", + "ws": "^8.13.0" } }, "node_modules/@ampproject/remapping": { @@ -9637,16 +9638,16 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -16771,9 +16772,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "dev": true, "requires": {} }, diff --git a/app/frontend/package.json b/app/frontend/package.json index 3215400f..3ecfaaaf 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -20,7 +20,8 @@ "webpack-bundle-tracker": "^1.7.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.11.1", - "webpack-notifier": "^1.15.0" + "webpack-notifier": "^1.15.0", + "ws": "^8.13.0" }, "license": "UNLICENSED", "private": true, diff --git a/app/frontend/webpack.config.js b/app/frontend/webpack.config.js index 6130f275..43eb8fcd 100644 --- a/app/frontend/webpack.config.js +++ b/app/frontend/webpack.config.js @@ -4,10 +4,58 @@ const BundleTracker = require('webpack-bundle-tracker'); const CopyPlugin = require("copy-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const Dotenv = require('dotenv-webpack'); +const WebSocket = require('ws'); + +require('dotenv').config({ path: '../../.env.local' }) const devMode = process.env.NODE_ENV !== "production"; const git_sha = process.env.GITHUB_SHA; +class CustomPlugin { + constructor(name, port = 9000, stage = 'afterEmit') { + this.name = name; + this.stage = stage; + try { + this.server = new WebSocket.Server({ + port: port + }); + let sockets = []; + this.server.on('connection', function(socket) { + sockets.push(socket); + + // When you receive a message, send that message to every socket. + socket.on('message', function(msg) { + sockets.forEach(s => s.send(msg)); + }); + + // When a socket closes, or disconnects, remove it from the array. + socket.on('close', function() { + sockets = sockets.filter(s => s !== socket); + }); + }); + } catch (error) { + console.error(error); + } + } + + apply(compiler) { + if (this.server.clients) { + compiler.hooks[this.stage].tap(this.name, () => { + try { + this.server.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send("reload"); + } + }); + } catch (error) { + console.error(error); + } + }); + } + } +} + + let config = { context: __dirname, mode: "development", @@ -38,28 +86,6 @@ let config = { }, module: { rules: [ - // { - // test: /\.css$/i, - // use: [MiniCssExtractPlugin.loader, "css-loader"], - // }, - // { - // test: /\.s[ac]ss$/i, - // use: [ - // // Creates `style` nodes from JS strings - // // "style-loader", - // // Translates CSS into CommonJS - // "css-loader", - // "sass-loader", - // // Compiles Sass to CSS - // // { - // // loader: "sass-loader", - // // options: { - // // // Prefer `dart-sass` - // // implementation: require("dart-sass"), - // // }, - // // }, - // ], - // }, { test: /\.(sa|sc|c)ss$/, use: [ @@ -83,56 +109,38 @@ let config = { } ] }, - // plugins: [ - // new MiniCssExtractPlugin(), - // new CopyPlugin({ - // patterns: [ - // { - // from: './assets/images/*.*', - // globOptions: { - // patterns: "*.+(png|jpg|jpeg|svg)", - // }, - // to: 'images/[path][name][ext]' - // }, - // { - // from: './assets/icons/*.svg', - // to: 'icons/[path][name][ext]' - // } - // ], - // }), - // new Dotenv(), - // new BundleTracker({filename: (argv.mode === 'production') ? '../static/webpack-stats.json' : './public/build/webpack-stats.json'}) - // ], - } module.exports = (env, argv) => { - if (argv.mode === 'development') { + if (argv.nodeEnv === 'development') { config.output.path = path.resolve('./public/build/') config.devtool = 'source-map'; config.output.filename = "[name].js"; } + config.plugins = [ new MiniCssExtractPlugin(), new CopyPlugin({ - patterns: [ - { - from: './assets/images/*.*', - globOptions: { - patterns: "*.+(png|jpg|jpeg|svg)", + patterns: [ + { + from: './assets/images/*.*', + globOptions: { + patterns: "*.+(png|jpg|jpeg|svg)", }, - to: 'images/[path][name][ext]' - }, - { - from: './assets/icons/*.svg', - to: 'icons/[path][name][ext]' - } + to: 'images/[path][name][ext]' + }, + { + from: './assets/icons/*.svg', + to: 'icons/[path][name][ext]' + } ], - }), - new Dotenv(), - new BundleTracker({filename: './public/build/webpack-stats.json'}) - ] -console.log(argv) + }), + new Dotenv({ path: '../../.env.local' }), + new BundleTracker({filename: './public/build/webpack-stats.json'}), + ] + if (argv.nodeEnv === 'development') { + config.plugins.push(new CustomPlugin('Reloader', process.env.DEV_SOCKET_PORT, 'done')) + } return config; diff --git a/app/requirements.txt b/app/requirements.txt index d0a921d3..7a64234d 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,7 @@ Django==3.2.16 +django-filter==22.1 djangorestframework==3.13.1 +drf-spectacular==0.26.0 uWSGI==2.0.20 django-webpack-loader==1.7.0 requests @@ -9,3 +11,7 @@ django_redis django-permissions-policy==4.14.0 django-cors-headers==3.13.0 django-csp==3.6 +weasyprint==52.5 +psycopg2-binary==2.9.5 +mozilla-django-oidc==3.0.0 +PyJWT<2.4.0 diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/utils/diversen.py b/app/utils/diversen.py new file mode 100644 index 00000000..8abd0f24 --- /dev/null +++ b/app/utils/diversen.py @@ -0,0 +1,16 @@ +def absolute(request): + urls = { + "ABSOLUTE_ROOT": request.build_absolute_uri("/")[:-1].strip("/"), + "FULL_URL_WITH_QUERY_STRING": request.build_absolute_uri(), + "FULL_URL": request.build_absolute_uri("?"), + } + + return urls + + +def gebruikersnaam(gebruiker): + if gebruiker.first_name or gebruiker.last_name: + first_name = gebruiker.first_name if gebruiker.first_name else "" + last_name = gebruiker.last_name if gebruiker.last_name else "" + return f"{first_name} {last_name}".strip() + return gebruiker.email diff --git a/dex.dev.yml b/dex.dev.yml new file mode 100644 index 00000000..b0e1bb00 --- /dev/null +++ b/dex.dev.yml @@ -0,0 +1,36 @@ +issuer: http://localhost:5556 + +storage: + type: sqlite3 + config: + file: /data/dex.db + +web: + http: 0.0.0.0:5556 + +staticClients: +- id: mor-regie + redirectURIs: + - http://localhost:8005/oidc/callback/ + - http://127.0.0.1:8005/oidc/callback/ + - http://regie.mor.local:8003/oidc/callback/ + name: Regie + secret: insecure + +enablePasswordDB: true + +staticPasswords: +- email: "user@example.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" # hash for: password + userID: "1" + username: "user" + name: "Standaard gebruiker" +- email: "admin@admin.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" # hash for: password + userID: "2" + username: "admin" + name: "Admin gebruiker" + +oauth2: + responseTypes: [ "code", "token", "id_token" ] + skipApprovalScreen: true diff --git a/docker-compose.sso-server.yml b/docker-compose.sso-server.yml new file mode 100644 index 00000000..d91797a9 --- /dev/null +++ b/docker-compose.sso-server.yml @@ -0,0 +1,80 @@ +version: '3' + +services: + db: + image: postgres:12.9 + environment: + PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - db_net + restart: unless-stopped + volumes: + - db_data:/var/lib/postgresql/data + + search: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + environment: + cluster.name: fusionauth + bootstrap.memory_lock: "true" + discovery.type: single-node + ES_JAVA_OPTS: ${ES_JAVA_OPTS} + healthcheck: + test: [ "CMD", "curl", "--fail" ,"--write-out", "'HTTP %{http_code}'", "--silent", "--output", "/dev/null", "http://localhost:9200/" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - search_net + restart: unless-stopped + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - search_data:/usr/share/elasticsearch/data + + fusionauth: + image: fusionauth/fusionauth-app:latest + depends_on: + db: + condition: service_healthy + search: + condition: service_healthy + environment: + DATABASE_URL: jdbc:postgresql://db:5432/fusionauth + DATABASE_ROOT_USERNAME: ${POSTGRES_USER} + DATABASE_ROOT_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_USERNAME: ${DATABASE_USERNAME} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + FUSIONAUTH_APP_MEMORY: ${FUSIONAUTH_APP_MEMORY} + FUSIONAUTH_APP_RUNTIME_MODE: development + FUSIONAUTH_APP_URL: http://fusionauth:9011 + SEARCH_SERVERS: http://search:9200 + SEARCH_TYPE: elasticsearch + + networks: + - db_net + - search_net + restart: unless-stopped + ports: + - 9011:9011 + volumes: + - fusionauth_config:/usr/local/fusionauth/config + +networks: + db_net: + driver: bridge + search_net: + driver: bridge + +volumes: + db_data: + fusionauth_config: + search_data: diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index b72471df..a0978181 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -3,24 +3,39 @@ services: app: build: ./app ports: - - "8000:8000" + - "8010:8000" env_file: - .env.test volumes: - ./app:/app depends_on: - - redis + database: + condition: service_healthy networks: - - mor_network - command: ["bash", "/app/deploy/docker-entrypoint.development.sh"] + - regie_network redis: image: redis networks: - - mor_network + - regie_network ports: - - "6379:6379" + - "7379:6379" + + database: + image: postgis/postgis:11-3.3 + shm_size: '1024m' + ports: + - "6432:5432" + env_file: + - .env.test + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - regie_network networks: - mor_network: + regie_network: external: true diff --git a/docker-compose.yaml b/docker-compose.yaml index 63fa26d1..e8cd5371 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,26 +1,77 @@ version: '3' services: + gateway: + container_name: regie.mor.local + build: ./nginx + volumes: + - ./nginx/nginx-default.development.conf:/etc/nginx/conf.d/default.conf + - ./media:/media + depends_on: + app: + condition: service_started + ports: + - 8003:8003 + networks: + mor_bridge_network: + env_file: + - .env.local app: + container_name: regie.app build: ./app ports: - - "8005:8000" + - "8000" env_file: - .env.local volumes: + - ./media:/media - ./app:/app depends_on: - - redis + redis: + condition: service_started + database: + condition: service_started networks: - - mor_network + - regie_network + - mor_bridge_network command: ["bash", "/app/deploy/docker-entrypoint.development.sh"] redis: image: redis networks: - - mor_network + - regie_network ports: - "6379" + database: + image: postgis/postgis:11-3.3 + shm_size: '1024m' + ports: + - "6003:5432" + env_file: + - .env.local + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - regie_network + + dex: + image: quay.io/dexidp/dex:v2.23.0 + user: root + command: serve /config.yml + ports: + - "7003:5556" + volumes: + - ./dex.dev.yml:/config.yml + - dex-data:/data + networks: + - regie_network + +volumes: + postgres-data: + dex-data: + networks: - mor_network: + regie_network: + external: true + mor_bridge_network: external: true diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..9ee7cd56 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx:alpine + +COPY ./nginx-default.conf /etc/nginx/conf.d/default.conf +COPY ./nginx-nginx.conf /etc/nginx/nginx.conf + +CMD exec nginx -g 'daemon off;' diff --git a/nginx/nginx-default.conf b/nginx/nginx-default.conf new file mode 100644 index 00000000..706579d0 --- /dev/null +++ b/nginx/nginx-default.conf @@ -0,0 +1,34 @@ +server { + listen 80 default_server; + server_name _; + + # These log files are softlinked to stdout and stderr + error_log /dev/stdout; + access_log /dev/stdout; + + client_max_body_size 41M; # 40MB for upload, 1MB for other data + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + location / { + proxy_pass http://localhost:8000; + } + + location /media-protected { + internal; + alias /media/; + access_log off; + expires 30d; + add_header Vary Accept-Encoding; + } + + location /static { + proxy_pass http://localhost:8000/static; + access_log off; + expires 30d; + add_header Vary Accept-Encoding; + add_header Access-Control-Allow-Origin *; + } +} diff --git a/nginx/nginx-default.development.conf b/nginx/nginx-default.development.conf new file mode 100644 index 00000000..b2bf400f --- /dev/null +++ b/nginx/nginx-default.development.conf @@ -0,0 +1,34 @@ +server { + listen 8003 default_server; + server_name _; + + # These log files are softlinked to stdout and stderr + error_log /dev/stdout; + access_log /dev/stdout; + + client_max_body_size 41M; # 40MB for upload, 1MB for other data + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + location / { + proxy_pass http://regie.app:8000; + } + + location /media-protected { + internal; + alias /media/; + access_log off; + expires 30d; + add_header Vary Accept-Encoding; + } + + location /static { + proxy_pass http://regie.app:8000/static; + access_log off; + expires 30d; + add_header Vary Accept-Encoding; + add_header Access-Control-Allow-Origin *; + } +} diff --git a/nginx/nginx-nginx.conf b/nginx/nginx-nginx.conf new file mode 100644 index 00000000..facc5686 --- /dev/null +++ b/nginx/nginx-nginx.conf @@ -0,0 +1,36 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + gzip on; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript image/svg+xml application/xml; + gzip_proxied no-cache no-store private expired auth; + gzip_min_length 1000; + + server_tokens off; + + include /etc/nginx/conf.d/*.conf; +}