Compare commits
137 Commits
ci
...
3eb7dbe984
Author | SHA1 | Date | |
---|---|---|---|
3eb7dbe984 | |||
202dfe46c2 | |||
01da0f1e29 | |||
8ccdf50bc5 | |||
d46ab8da6b | |||
1dd53a87e9 | |||
40bb2e54bd | |||
433ad9d4b9 | |||
231c27819d | |||
890309564f | |||
e1e1f822c8 | |||
7a788f4c90 | |||
7efa626b8b | |||
08e20e1875 | |||
f1c79a5f94 | |||
5dd1991af8 | |||
c0edef51bd | |||
cb703e79ae | |||
87066b0cea | |||
c4976c4b34 | |||
ee46ff9cda | |||
d4f27e8f2f | |||
4a6584370e | |||
82d3f95c99 | |||
dce3d89c7e | |||
5520590145 | |||
efabebfdbf | |||
6c52246bb7 | |||
2c11f7c385 | |||
9ee0bd8e30 | |||
1955476d24 | |||
05178da029 | |||
7a80cf8df1 | |||
db94ec41ed | |||
5582538a70 | |||
7aa364fc38 | |||
96ce5963fe | |||
bf54bc5d51 | |||
93ae172431 | |||
03d40a5092 | |||
993f8f9cd2 | |||
8efc0aad21 | |||
3a6e7f5344 | |||
dac9661d51 | |||
b9bfa8e359 | |||
d07589464c | |||
1880da5151 | |||
4e953c83ea | |||
2212df4729 | |||
98d67381c6 | |||
e02672c2bb | |||
c3dd9faa85 | |||
9f977e35c2 | |||
3269d5a39a | |||
d96a44bbdd | |||
2641b2e7bf | |||
50c1a4f2c6 | |||
573630f9ee | |||
1a09b7859f | |||
70b3ae4bbc | |||
5eaafe7646 | |||
5781b49c7c | |||
e2f516d409 | |||
ca8996fff6 | |||
eb734d2716 | |||
655e304c6c | |||
8e34ed440e | |||
0c7080f005 | |||
0b93b5eccb | |||
f1d9f7ad22 | |||
4e71ac7866 | |||
1d0a42a7e1 | |||
d384e75746 | |||
70154abd37 | |||
ab3437e61d | |||
0ccbb18411 | |||
e6f12ce5b1 | |||
6325de17d9 | |||
b9d6293546 | |||
dbe52e4884 | |||
3c286d84d8 | |||
227fa4d5a8 | |||
d47f181e1d | |||
272046142e | |||
5c18832961 | |||
d59cc0034a | |||
64024be833 | |||
5ef20bdce0 | |||
7ddd7b0c0c | |||
cbd8700917 | |||
6eb2f5000f | |||
1cd70228b9 | |||
23d8e85031 | |||
4fb92d8215 | |||
6dfc92bf15 | |||
2015f8b332 | |||
66a0b42718 | |||
efecfc910d | |||
96bc44c508 | |||
a2c8f469a7 | |||
a98b428614 | |||
dfede77e98 | |||
6702211c05 | |||
f97e682640 | |||
c1e3248cc8 | |||
0e67b777b5 | |||
0435c427b3 | |||
be2df6970a | |||
1f5e7856b1 | |||
793de1ec64 | |||
6844e771b5 | |||
1282b6b201 | |||
975de1a230 | |||
fd3478600f | |||
4d2991ba2f | |||
5aaaf57dd4 | |||
766b19e7c2 | |||
f660a6b49a | |||
ab0c1a5c46 | |||
74a6b5f2aa | |||
e38234b736 | |||
ce38002676 | |||
314cdfdd7c | |||
4504a18f60 | |||
df41028e99 | |||
f404cfa0a3 | |||
72dedb6b0c | |||
17468097ec | |||
28331f105a | |||
39893c2185 | |||
ab2b91735e | |||
1b9574cca9 | |||
0d52101f22 | |||
96c0c1218f | |||
864c76bc21 | |||
c3646e6334 | |||
83a219df0c |
@@ -1,10 +1,12 @@
|
||||
FROM python:3-slim
|
||||
FROM python:3.11-slim
|
||||
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
|
||||
MAINTAINER Julian-Samuel Gebühr
|
||||
|
||||
ENV DOCKER_BUILD=true
|
||||
|
||||
RUN apt update
|
||||
RUN apt install gettext -y
|
||||
RUN apt install libpq-dev gcc -y
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN mkdir /app/data
|
||||
|
35
README.md
35
README.md
@@ -44,14 +44,16 @@ nf query_location <query>
|
||||
There is a system for customizing texts in Notfellchen. Not every change of a tet should mean an update of the software. But this should also not become a CMS.
|
||||
Therefore, a solution is used where a number of predefined texts per site are supported. These markdown texts will then be included in the site, if defined.
|
||||
|
||||
| Textcode | Location |
|
||||
|---------------------|----------|
|
||||
| `how_to` | Index |
|
||||
| `introduction` | Index |
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| Any rule | About |
|
||||
| Textcode | Location |
|
||||
|-------------------------|-----------------------|
|
||||
| `how_to` | Index |
|
||||
| `introduction` | Index |
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| `about_us` | About |
|
||||
| `external_site_warning` | External Site Warning |
|
||||
| Any rule | About |
|
||||
|
||||
# Developer Notes
|
||||
|
||||
@@ -106,3 +108,20 @@ Use a program like `gtranslator` or `poedit` to start translations
|
||||
| Edit adoption notice | User that created, Moderator, Admin |
|
||||
| Edit animal | User that created, Moderator, Admin |
|
||||
| Add animal/photo to adoption notice | User that created, Moderator, Admin |
|
||||
|
||||
# Celery and KeyDB
|
||||
|
||||
Start KeyDB docker container
|
||||
```zsh
|
||||
docker run -d --name keydb -p 6379:6379 eqalpha/keydb
|
||||
```
|
||||
|
||||
Start worker
|
||||
```zsh
|
||||
celery -A notfellchen.celery worker
|
||||
```
|
||||
|
||||
Start beat
|
||||
```zsh
|
||||
celery -A notfellchen.celery beat
|
||||
```
|
||||
|
@@ -4,9 +4,5 @@ Administration
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
create_user.rst
|
||||
lending.rst
|
||||
returning.rst
|
||||
opening_hours.rst
|
||||
add_items.rst
|
||||
GDPR.rst
|
||||
monitoring.rst
|
||||
|
@@ -1,7 +1,7 @@
|
||||
Monitoring
|
||||
==========
|
||||
|
||||
ILMO should, like every other software, be easy to monitor. Therefore a basic metrics are exposed to `https://notfellchen.org/metrics`.
|
||||
Notfellchen should, like every other software, be easy to monitor. Therefore a basic metrics are exposed to `https://notfellchen.org/metrics`.
|
||||
The data is encoded in JSON format and is therefore suitable to bea read by humans and it is easy to use it as data source for further processing.
|
||||
|
||||
|
||||
@@ -60,3 +60,12 @@ Now we can simply use the InfluxDB as data source in Grafana and configure until
|
||||
beautiful plots!
|
||||
|
||||
.. image:: monitoring_grafana.png
|
||||
|
||||
Healthchecks
|
||||
------------
|
||||
|
||||
You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running.
|
||||
Add the following to your `notfellchen.cfg` and adjust the URL to match your check.
|
||||
.. code::
|
||||
[monitoring]
|
||||
healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
|
||||
|
@@ -1,10 +0,0 @@
|
||||
Opening hours
|
||||
=============
|
||||
|
||||
The opening hours can be changed by selecting the page :guilabel:`Opening hours` in the navigation menu.
|
||||
You can not change an entry, simply delete it and create a new one.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
It is advised to fill empty time cells with a "-".
|
@@ -1,8 +0,0 @@
|
||||
Returning
|
||||
=========
|
||||
|
||||
To return an item either visit the page :guilabel:`All loans` and search
|
||||
for the loan there or you search for the item via :guilabel:`Search`.
|
||||
|
||||
If you found the loan, you can simply click on the button :guilabel:`Return` and
|
||||
you are finished.
|
@@ -20,7 +20,7 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Notfellchen'
|
||||
copyright = 'Julian-Samuel Gebühr'
|
||||
copyright = 'CC-BY-SA Julian-Samuel Gebühr'
|
||||
author = 'Julian-Samuel Gebühr'
|
||||
|
||||
# The short X.Y version
|
||||
|
@@ -4,16 +4,16 @@
|
||||
Deployment
|
||||
**********
|
||||
|
||||
There are different ways to deploy ILMO. We support an ansible+docker based deployment and manual installation.
|
||||
There are different ways to deploy Notfellchen. We support an ansible+docker based deployment and manual installation.
|
||||
|
||||
Ansible deployment
|
||||
==================
|
||||
|
||||
ILMO can be deployed with the `ilmo-ansible-role <https://github.com/moan0s/ansible-role-ilmo>`_ that is based on the
|
||||
official ILMO docker image. This role will only install ilmo itself. If you want a complete setup that includes a
|
||||
Notfellchen can be deployed with the `notfellchen-ansible-role <https://github.com/moan0s/ansible-role-notfellchen>`_ that is based on the
|
||||
official Notfellchen docker image. This role will only install notfellchen itself. If you want a complete setup that includes a
|
||||
database and a webserver with minimal configuration you can use the
|
||||
`mash-playbook <https://github.com/mother-of-all-self-hosting/mash-playbook>`_ by following `it's documentation
|
||||
on ILMO <https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/ilmo.md>`_.
|
||||
on Notfellchen <https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/notfellchen.md>`_.
|
||||
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ Manual Deployment
|
||||
=================
|
||||
|
||||
|
||||
This guide describes the installation of a installation of ILMO from source. It is inspired by this great guide from
|
||||
This guide describes the installation of a installation of Notfellchen from source. It is inspired by this great guide from
|
||||
pretix_.
|
||||
|
||||
.. warning:: Even though this guide tries to make it as straightforward to run ILMO, it still requires some Linux experience to
|
||||
.. warning:: Even though this guide tries to make it as straightforward to run Notfellchen, it still requires some Linux experience to
|
||||
get it right. If you're not feeling comfortable managing a Linux server, check out a managed service_.
|
||||
|
||||
This guide is tested on **Ubuntu20.04** but it should work very similar on other modern systemd based distributions.
|
||||
@@ -39,18 +39,18 @@ installation guides):
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Traefik to allow HTTPS connections
|
||||
* A `PostgreSQL`_ database server
|
||||
|
||||
Also recommended is, that you use a firewall, although this is not a ILMO-specific recommendation. If you're new to
|
||||
Also recommended is, that you use a firewall, although this is not a Notfellchen-specific recommendation. If you're new to
|
||||
Linux and firewalls, it is recommended that you start with `ufw`_.
|
||||
|
||||
.. note:: Please, do not run ILMO without HTTPS encryption. You'll handle user data and thanks to `Let's Encrypt`_
|
||||
.. note:: Please, do not run Notfellchen without HTTPS encryption. You'll handle user data and thanks to `Let's Encrypt`_
|
||||
SSL certificates can be obtained for free these days.
|
||||
|
||||
Unix user
|
||||
---------
|
||||
|
||||
As we do not want to run ilmo as root, we first create a new unprivileged user::
|
||||
As we do not want to run notfellchen as root, we first create a new unprivileged user::
|
||||
|
||||
# adduser ilmo --disabled-password --home /var/ilmo
|
||||
# adduser notfellchen --disabled-password --home /var/notfellchen
|
||||
|
||||
In this guide, all code lines prepended with a ``#`` symbol are commands that you need to execute on your server as
|
||||
``root`` user (e.g. using ``sudo``); all lines prepended with a ``$`` symbol should be run by the unprivileged user.
|
||||
@@ -66,16 +66,16 @@ best compatibility. You can check this with the following command::
|
||||
|
||||
For PostgreSQL database creation, we would do::
|
||||
|
||||
# sudo -u postgres createuser ilmo
|
||||
# sudo -u postgres createdb -O ilmo ilmo
|
||||
# su ilmo
|
||||
# sudo -u postgres createuser notfellchen
|
||||
# sudo -u postgres createdb -O notfellchen notfellchen
|
||||
# su notfellchen
|
||||
$ psql
|
||||
> ALTER USER ilmo PASSWORD 'strong_password';
|
||||
> ALTER USER notfellchen PASSWORD 'strong_password';
|
||||
|
||||
Package dependencies
|
||||
--------------------
|
||||
|
||||
To build and run ilmo, you will need the following debian packages::
|
||||
To build and run notfellchen, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
||||
python3-dev
|
||||
@@ -83,32 +83,32 @@ To build and run ilmo, you will need the following debian packages::
|
||||
Config file
|
||||
-----------
|
||||
|
||||
We now create a config directory and config file for ilmo::
|
||||
We now create a config directory and config file for notfellchen::
|
||||
|
||||
# mkdir /etc/ilmo
|
||||
# touch /etc/ilmo/ilmo.cfg
|
||||
# chown -R ilmo:ilmo /etc/ilmo/
|
||||
# chmod 0600 /etc/ilmo/ilmo.cfg
|
||||
# mkdir /etc/notfellchen
|
||||
# touch /etc/notfellchen/notfellchen.cfg
|
||||
# chown -R notfellchen:notfellchen /etc/notfellchen/
|
||||
# chmod 0600 /etc/notfellchen/notfellchen.cfg
|
||||
|
||||
Fill the configuration file ``/etc/ilmo/ilmo.cfg`` with the following content (adjusted to your environment)::
|
||||
Fill the configuration file ``/etc/notfellchen/notfellchen.cfg`` with the following content (adjusted to your environment)::
|
||||
|
||||
[ilmo]
|
||||
[notfellchen]
|
||||
instance_name=My library
|
||||
url=https://ilmo.example.com
|
||||
url=https://notfellchen.example.com
|
||||
|
||||
[database]
|
||||
backend=postgresql
|
||||
name=ilmo
|
||||
user=ilmo
|
||||
name=notfellchen
|
||||
user=notfellchen
|
||||
|
||||
[locations]
|
||||
static=/var/ilmo/static
|
||||
static=/var/notfellchen/static
|
||||
|
||||
[mail]
|
||||
; See config file documentation for more options
|
||||
; from=ilmo@example.com
|
||||
; from=notfellchen@example.com
|
||||
; host=127.0.0.1
|
||||
; user=ilmo
|
||||
; user=notfellchen
|
||||
; password=foobar
|
||||
; port=587
|
||||
|
||||
@@ -121,21 +121,21 @@ Fill the configuration file ``/etc/ilmo/ilmo.cfg`` with the following content (a
|
||||
;Scope=
|
||||
;Policy=
|
||||
|
||||
Install ilmo as package
|
||||
Install notfellchen as package
|
||||
------------------------
|
||||
|
||||
Now we will install ilmo itself. The following steps are to be executed as the ``ilmo`` user. Before we
|
||||
actually install ilmo, we will create a virtual environment to isolate the python packages from your global
|
||||
Now we will install notfellchen itself. The following steps are to be executed as the ``notfellchen`` user. Before we
|
||||
actually install notfellchen, we will create a virtual environment to isolate the python packages from your global
|
||||
python installation::
|
||||
|
||||
$ python3 -m venv /var/ilmo/venv
|
||||
$ source /var/ilmo/venv/bin/activate
|
||||
$ python3 -m venv /var/notfellchen/venv
|
||||
$ source /var/notfellchen/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
|
||||
We now clone and install ilmo, its direct dependencies and gunicorn::
|
||||
We now clone and install notfellchen, its direct dependencies and gunicorn::
|
||||
|
||||
(venv)$ git clone https://github.com/moan0s/ILMO2
|
||||
(venv)$ cd ILMO2/src/
|
||||
(venv)$ git clone https://github.com/moan0s/Notfellchen2
|
||||
(venv)$ cd Notfellchen2/src/
|
||||
(venv)$ pip3 install -r requirements.txt
|
||||
(venv)$ pip3 install -e .
|
||||
|
||||
@@ -148,26 +148,26 @@ Finally, we compile static files and create the database structure::
|
||||
(venv)$ django-admin compilemessages --ignore venv
|
||||
|
||||
|
||||
Start ilmo as a service
|
||||
Start notfellchen as a service
|
||||
-------------------------
|
||||
|
||||
You should start ilmo using systemd to automatically start it after a reboot. Create a file
|
||||
named ``/etc/systemd/system/ilmo-web.service`` with the following content::
|
||||
You should start notfellchen using systemd to automatically start it after a reboot. Create a file
|
||||
named ``/etc/systemd/system/notfellchen-web.service`` with the following content::
|
||||
|
||||
[Unit]
|
||||
Description=ilmo web service
|
||||
Description=notfellchen web service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=ilmo
|
||||
Group=ilmo
|
||||
Environment="VIRTUAL_ENV=/var/ilmo/venv"
|
||||
Environment="PATH=/var/ilmo/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/var/ilmo/venv/bin/gunicorn ilmo.wsgi \
|
||||
--name ilmo --workers 5 \
|
||||
User=notfellchen
|
||||
Group=notfellchen
|
||||
Environment="VIRTUAL_ENV=/var/notfellchen/venv"
|
||||
Environment="PATH=/var/notfellchen/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/var/notfellchen/venv/bin/gunicorn notfellchen.wsgi \
|
||||
--name notfellchen --workers 5 \
|
||||
--max-requests 1200 --max-requests-jitter 50 \
|
||||
--log-level=info --bind=127.0.0.1:8345
|
||||
WorkingDirectory=/var/ilmo
|
||||
WorkingDirectory=/var/notfellchen
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
@@ -176,14 +176,14 @@ named ``/etc/systemd/system/ilmo-web.service`` with the following content::
|
||||
You can now run the following commands to enable and start the services::
|
||||
|
||||
# systemctl daemon-reload
|
||||
# systemctl enable ilmo-web
|
||||
# systemctl start ilmo-web
|
||||
# systemctl enable notfellchen-web
|
||||
# systemctl start notfellchen-web
|
||||
|
||||
|
||||
SSL
|
||||
---
|
||||
|
||||
The following snippet is an example on how to configure a nginx proxy for ilmo::
|
||||
The following snippet is an example on how to configure a nginx proxy for notfellchen::
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
@@ -196,8 +196,8 @@ The following snippet is an example on how to configure a nginx proxy for ilmo::
|
||||
#
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/ilmo.example.com/cert.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/ilmo.example.com/privkey.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/notfellchen.example.com/cert.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/notfellchen.example.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
@@ -208,7 +208,7 @@ The following snippet is an example on how to configure a nginx proxy for ilmo::
|
||||
add_header Referrer-Policy same-origin;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
server_name ilmo.example.com;
|
||||
server_name notfellchen.example.com;
|
||||
location / {
|
||||
proxy_pass http://localhost:8345;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -217,7 +217,7 @@ The following snippet is an example on how to configure a nginx proxy for ilmo::
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /var/ilmo/static/;
|
||||
alias /var/notfellchen/static/;
|
||||
access_log off;
|
||||
expires 365d;
|
||||
add_header Cache-Control "public";
|
||||
@@ -230,22 +230,22 @@ We recommend reading about setting `strong encryption settings`_ for your web se
|
||||
Next steps
|
||||
----------
|
||||
|
||||
Yay, you are done! You should now be able to reach ilmo at https://ilmo.example.com/
|
||||
Yay, you are done! You should now be able to reach notfellchen at https://notfellchen.example.com/
|
||||
|
||||
Updates
|
||||
-------
|
||||
|
||||
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
||||
|
||||
To upgrade to a new ilmo release, pull the latest code changes and run the following commands::
|
||||
To upgrade to a new notfellchen release, pull the latest code changes and run the following commands::
|
||||
|
||||
$ source /var/ilmo/venv/bin/activate
|
||||
$ source /var/notfellchen/venv/bin/activate
|
||||
(venv)$ git pull
|
||||
(venv)$ pg_dump ilmo > ilmo.psql
|
||||
(venv)$ pg_dump notfellchen > notfellchen.psql
|
||||
(venv)$ python manage.py migrate
|
||||
(venv)$ django-admin compilemessages --ignore venv
|
||||
|
||||
# systemctl restart ilmo-web
|
||||
# systemctl restart notfellchen-web
|
||||
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||
|
@@ -8,5 +8,6 @@ Installation, customization and contributing
|
||||
|
||||
deployment.rst
|
||||
contributing.rst
|
||||
translation.rst
|
||||
release.rst
|
||||
backup.rst
|
||||
|
@@ -12,10 +12,7 @@ Notfellchen Plattform Dokumentation
|
||||
API/index.rst
|
||||
|
||||
.. image:: rtfm.png
|
||||
:name: RTFM by Elektroll
|
||||
:scale: 50 %
|
||||
:alt: Soviet style image of workers holding a sign with a gear and a screwdriver. Below is says "Read the manual"
|
||||
:name: Ratte lesend
|
||||
:alt: Zeichnung einer lesenden Ratte
|
||||
:align: center
|
||||
|
||||
|
||||
Read the manual, Image by `Mike Powell (CC-BY) <https://elektroll.art/>`_.
|
||||
|
BIN
docs/rtfm.png
BIN
docs/rtfm.png
Binary file not shown.
Before Width: | Height: | Size: 815 KiB After Width: | Height: | Size: 485 KiB |
BIN
docs/user/abonnieren.png
Normal file
BIN
docs/user/abonnieren.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@@ -1,9 +1,19 @@
|
||||
Benachrichtigungen
|
||||
==================
|
||||
|
||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
|
||||
Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben dem Titel der Vermittlung.
|
||||
|
||||
.. image:: abonnieren.png
|
||||
|
||||
Auf der Website
|
||||
+++++++++++++++
|
||||
|
||||
|
||||
|
||||
E-Mail
|
||||
++++++
|
||||
|
||||
Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an.
|
||||
Wir senden dir Benachrichtigungen an diese E-Mail. Du kannst das in deinen Profileinstellungen anpassen.
|
||||
|
||||
Benachrichtigungen senden wir per Mail - du kannst das jederzeit in den Einstellungen deaktivieren.
|
@@ -1,11 +1,11 @@
|
||||
***********
|
||||
Users guide
|
||||
***********
|
||||
******************
|
||||
User Dokumentation
|
||||
******************
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
:caption: Inhalt:
|
||||
|
||||
registrierung.rst
|
||||
vermittlungen.rst
|
||||
moderationskonzept.rst
|
||||
benachrichtigungen.rst
|
||||
login.rst
|
||||
email.rst
|
||||
|
29
docs/user/moderationskonzept.rst
Normal file
29
docs/user/moderationskonzept.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
Moderationskonzept
|
||||
==================
|
||||
|
||||
Vertrauen in notfellchen.org ist uns wichtig. Unser Kernziel ist es Tierschutz und Tierwohl zu fördern. Dafür sind drei
|
||||
Grundkonzepte wichtig
|
||||
|
||||
* Aktualität: Informationen auf notfellchen.org müssen aktuell&richtig sein
|
||||
* Tierschutz: Ausschließlich Ratten aus dem Tierschutz werden vermittelt
|
||||
* Moderation: Vermittlungen und Kommentare können gemeldet werden und werden vom Team zügig moderiert.
|
||||
|
||||
Vermittlungen
|
||||
+++++++++++++
|
||||
|
||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
|
||||
|
||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
|
||||
Nach 3 Wochen ohne Prüfung werden Anzeigen automatisch von der Seite entfernt und nur dann wieder freigeschaltet, wenn eine manuelle Prüfung erfolgt.
|
||||
|
||||
Darüber hinaus werden einmal täglich die verlinkten Seiten automatisiert geprüft. Wenn eine Vermittlung auf der Website eines Tierheims oder einer Pflegestelle entfernt wird, wird die Anzeige sofort deaktiviert.
|
||||
|
||||
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund für eine Meldung kann sein, dass Informationen veraltet sind oder ein Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
|
||||
|
||||
Kommentare
|
||||
++++++++++
|
||||
|
||||
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
|
||||
|
||||
Kommentare können, wie Vermittlungen, gemeldet werden wenn sie nicht den Regeln entsprechen.
|
@@ -1,5 +1,10 @@
|
||||
Registration
|
||||
Registrierung
|
||||
================================
|
||||
|
||||
To register you have to visit the library. An librarian will then set up an account for you.
|
||||
You will need to provide an valid E-Mail Address and a password.
|
||||
Du kannst dich jederzeit selbst registrieren. Das geht unter https://notfellchen.org/accounts/register/
|
||||
|
||||
Ein Account ermöglicht es dir
|
||||
|
||||
* Kommentare zu hinterlassen
|
||||
* Vermittlungen hinzuzufügen
|
||||
* Vermittlungen zu abonnieren
|
@@ -1,3 +1,17 @@
|
||||
Vermittlungen
|
||||
=============
|
||||
|
||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
|
||||
|
||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
|
||||
Nach 3 Wochen ohne Prüfung werden Anzeigen automatisch von der Seite entfernt und nur dann wieder freigeschaltet, wenn eine manuelle Prüfung erfolgt.
|
||||
|
||||
Darüber hinaus werden einmal täglich die verlinkten Seiten automatisiert geprüft. Wenn eine Vermittlungs-Seite bei einem Tierheim oder einer Pflegestelle entfernt wurde, wird die Anzeige ebenfalls deaktiviert.
|
||||
|
||||
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund dafür kann sein, dass Informationen veraltet sind oder ein Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
|
||||
|
||||
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
|
||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
|
||||
|
||||
Kommentare können, wie Vermittlungen, gemeldet werden.
|
||||
|
@@ -38,10 +38,17 @@ dependencies = [
|
||||
"psycopg2-binary",
|
||||
"django-crispy-forms",
|
||||
"crispy-bootstrap4",
|
||||
"djangorestframework"
|
||||
"djangorestframework",
|
||||
"celery[redis]"
|
||||
]
|
||||
|
||||
dynamic = ["version", "readme"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://notfellchen.org"
|
||||
repository = "https://codeberg.org/moanos/notfellchen/"
|
||||
|
@@ -1,11 +1,16 @@
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
import csv
|
||||
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice
|
||||
from django.contrib import admin
|
||||
from django.http import HttpResponse
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp
|
||||
|
||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class StatusInline(admin.StackedInline):
|
||||
@@ -14,14 +19,45 @@ class StatusInline(admin.StackedInline):
|
||||
|
||||
@admin.register(AdoptionNotice)
|
||||
class AdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains", "description__icontains")
|
||||
list_filter = ("owner",)
|
||||
inlines = [
|
||||
StatusInline,
|
||||
]
|
||||
|
||||
|
||||
# Re-register UserAdmin
|
||||
admin.site.register(User)
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains")
|
||||
list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices")
|
||||
list_filter = ("is_active", "trust_level",)
|
||||
actions = ("export_as_csv",)
|
||||
|
||||
def view_adoption_notices(self, obj):
|
||||
count = obj.adoption_notices.count()
|
||||
url = (
|
||||
reverse("admin:fellchensammlung_adoptionnotice_changelist")
|
||||
+ "?"
|
||||
+ urlencode({"owner__id": f"{obj.id}"})
|
||||
)
|
||||
return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
|
||||
|
||||
def export_as_csv(self, request, queryset):
|
||||
meta = self.model._meta
|
||||
field_names = [field.name for field in meta.fields]
|
||||
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
|
||||
writer = csv.writer(response)
|
||||
|
||||
writer.writerow(field_names)
|
||||
for obj in queryset:
|
||||
row = writer.writerow([getattr(obj, field) for field in field_names])
|
||||
|
||||
return response
|
||||
|
||||
export_as_csv.short_description = _("Ausgewählte User exportieren")
|
||||
|
||||
def _reported_content_link(obj):
|
||||
reported_content = obj.reported_content
|
||||
@@ -50,15 +86,30 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
reported_content_link.short_description = "Reported Content"
|
||||
|
||||
|
||||
@admin.register(RescueOrganization)
|
||||
class RescueOrganizationAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains",)
|
||||
list_display = ("name", "trusted", "allows_using_materials", "website")
|
||||
list_filter = ("allows_using_materials", "trusted",)
|
||||
|
||||
|
||||
@admin.register(Text)
|
||||
class TextAdmin(admin.ModelAdmin):
|
||||
search_fields = ("title__icontains", "text_code__icontains",)
|
||||
|
||||
@admin.register(Comment)
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user",)
|
||||
|
||||
admin.site.register(Animal)
|
||||
admin.site.register(Species)
|
||||
admin.site.register(RescueOrganization)
|
||||
admin.site.register(Location)
|
||||
admin.site.register(Rule)
|
||||
admin.site.register(Image)
|
||||
admin.site.register(ModerationAction)
|
||||
admin.site.register(Language)
|
||||
admin.site.register(Text)
|
||||
admin.site.register(Announcement)
|
||||
admin.site.register(AdoptionNoticeStatus)
|
||||
admin.site.register(Subscriptions)
|
||||
admin.site.register(Log)
|
||||
admin.site.register(Timestamp)
|
||||
|
@@ -15,3 +15,4 @@ class FellchensammlungConfig(AppConfig):
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
post_migrate.connect(ensure_languages, sender=self)
|
||||
import fellchensammlung.receivers
|
||||
|
@@ -1,16 +1,22 @@
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||
Comment
|
||||
from django_registration.forms import RegistrationForm
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field
|
||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from notfellchen.settings import MEDIA_URL
|
||||
|
||||
|
||||
def animal_validator(value: str):
|
||||
value = value.lower()
|
||||
animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat",
|
||||
"dog", "rabbit", "fox", "fancy rat"]
|
||||
if value not in animal_list:
|
||||
raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes"))
|
||||
|
||||
|
||||
class DateInput(forms.DateInput):
|
||||
input_type = 'date'
|
||||
|
||||
@@ -45,6 +51,7 @@ class AdoptionNoticeForm(forms.ModelForm):
|
||||
'group_only',
|
||||
'searching_since',
|
||||
'location_string',
|
||||
'organization',
|
||||
'description',
|
||||
'further_information',
|
||||
),
|
||||
@@ -52,19 +59,19 @@ class AdoptionNoticeForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
|
||||
"organization"]
|
||||
|
||||
|
||||
class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string", "organization"]
|
||||
widgets = {
|
||||
'searching_since': DateInput(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AnimalForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||
@@ -93,6 +100,7 @@ class AnimalFormWithDateWidget(AnimalForm):
|
||||
'date_of_birth': DateInput(),
|
||||
}
|
||||
|
||||
|
||||
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
|
||||
@@ -142,8 +150,8 @@ class CommentForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_action = "comment"
|
||||
self.helper.form_class = 'form-comments'
|
||||
self.helper.add_input(Hidden('action', 'comment'))
|
||||
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
||||
|
||||
class Meta:
|
||||
@@ -161,6 +169,8 @@ class CustomRegistrationForm(RegistrationForm):
|
||||
class Meta(RegistrationForm.Meta):
|
||||
model = User
|
||||
|
||||
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
@@ -171,9 +181,9 @@ class CustomRegistrationForm(RegistrationForm):
|
||||
|
||||
|
||||
def _get_distances():
|
||||
return {i: i for i in [10, 20, 50, 100, 200, 500]}
|
||||
return {i: i for i in [20, 50, 100, 200, 500]}
|
||||
|
||||
|
||||
class AdoptionNoticeSearchForm(forms.Form):
|
||||
postcode = forms.CharField(max_length=20, label=_("Postleitzahl"))
|
||||
location = forms.CharField(max_length=20, label=_("Stadt"))
|
||||
max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import django.conf.global_settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.db.models import Q, Min
|
||||
from fellchensammlung.models import User
|
||||
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
|
||||
from notfellchen.settings import host
|
||||
|
||||
NEWLINE = "\r\n"
|
||||
|
||||
|
||||
def mail_admins_new_report(report):
|
||||
subject = _("Neue Meldung")
|
||||
for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
|
||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||
greeting = _("Moin,") + "{NEWLINE}"
|
||||
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
|
||||
if len(report.reported_broken_rules.all()) > 0:
|
||||
@@ -29,5 +29,15 @@ def mail_admins_new_report(report):
|
||||
link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
|
||||
body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text
|
||||
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
|
||||
print("Sending email to ", moderator.email)
|
||||
message.send()
|
||||
|
||||
|
||||
def send_notification_email(notification_pk):
|
||||
try:
|
||||
notification = CommentNotification.objects.get(pk=notification_pk)
|
||||
except CommentNotification.DoesNotExist:
|
||||
notification = BaseNotification.objects.get(pk=notification_pk)
|
||||
subject = f"🔔 {notification.title}"
|
||||
body_text = notification.text
|
||||
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
|
||||
message.send()
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from django.core.management import BaseCommand
|
||||
from fellchensammlung.models import AdoptionNotice, Location
|
||||
from fellchensammlung.tools.geo import clean_locations
|
||||
from fellchensammlung.tools.admin import clean_locations
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,4 +15,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
clean_locations(quiet=False)
|
||||
clean_locations(quiet=False)
|
||||
|
@@ -7,7 +7,7 @@ from fellchensammlung import baker_recipes
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \
|
||||
Report, Comment, ReportAdoptionNotice
|
||||
Report, Comment, ReportAdoptionNotice, TrustLevel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -101,10 +101,10 @@ class Command(BaseCommand):
|
||||
|
||||
User.objects.create_user('test', password='foobar')
|
||||
admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org",
|
||||
trust_level=User.TRUST_LEVEL[User.ADMIN])
|
||||
trust_level=TrustLevel.ADMIN)
|
||||
|
||||
mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org",
|
||||
trust_level=User.TRUST_LEVEL[User.MODERATOR])
|
||||
trust_level=TrustLevel.MODERATOR)
|
||||
|
||||
comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1)
|
||||
comment2 = baker.make(Comment,
|
||||
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-10 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0007_alter_adoptionnotice_last_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='minor_status',
|
||||
field=models.CharField(choices=[('searching', 'searching'), ('interested', 'interested'), ('waiting_for_review', 'waiting_for_review'), ('needs_additional_info', 'needs_additional_info'), ('successful_with_notfellchen', 'successful_with_notfellchen'), ('successful_without_notfellchen', 'successful_without_notfellchen'), ('animal_died', 'animal_died'), ('closed_for_other_adoption_notice', 'closed_for_other_adoption_notice'), ('not_open_for_adoption_anymore', 'not_open_for_adoption_anymore'), ('other', 'other'), ('against_the_rules', 'against_the_rules'), ('missing_information', 'missing_information'), ('technical_error', 'technical_error'), ('unchecked', 'unchecked')], max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='announcement',
|
||||
name='publish_start_time',
|
||||
field=models.DateTimeField(verbose_name='Veröffentlichungszeitpunkt'),
|
||||
),
|
||||
]
|
25
src/fellchensammlung/migrations/0009_log.py
Normal file
25
src/fellchensammlung/migrations/0009_log.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-10 21:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0008_alter_adoptionnoticestatus_minor_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Log',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=255, verbose_name='Aktion')),
|
||||
('text', models.CharField(max_length=1000, verbose_name='Log text')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
|
||||
],
|
||||
),
|
||||
]
|
21
src/fellchensammlung/migrations/0010_timestamp.py
Normal file
21
src/fellchensammlung/migrations/0010_timestamp.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-19 18:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0009_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Timestamp',
|
||||
fields=[
|
||||
('key', models.CharField(max_length=255, primary_key=True, serialize=False, verbose_name='Schlüssel')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Zeitstempel')),
|
||||
('data', models.CharField(blank=True, max_length=2000, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-29 10:44
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0010_timestamp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='created_at',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Zuletzt überprüft am'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,136 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-03 20:07
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0011_alter_adoptionnotice_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='basenotification',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='log',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderationaction',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='report',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscriptions',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-06 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0012_adoptionnotice_updated_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='log',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-07 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0013_alter_log_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-09 09:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0014_rescueorganization_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Kommentar'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-13 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0015_rescueorganization_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 06:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0016_rescueorganization_phone_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='organization_affiliation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization', verbose_name='Organisation'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 17:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0017_user_organization_affiliation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Beschreibung'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 18:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0018_rescueorganization_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='rescueorganization',
|
||||
old_name='comment',
|
||||
new_name='internal_comment',
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 18:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0019_rename_comment_rescueorganization_internal_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='internal_comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Interner Kommentar'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 20:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0020_alter_rescueorganization_internal_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='reason_for_signup',
|
||||
field=models.TextField(default='-', verbose_name='Grund für die Registrierung'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-20 18:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0021_user_reason_for_signup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='reason_for_signup',
|
||||
field=models.TextField(help_text="Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='trust_level',
|
||||
field=models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-20 19:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0022_alter_user_reason_for_signup_alter_user_trust_level'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='email_notifications',
|
||||
field=models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail'),
|
||||
),
|
||||
]
|
@@ -3,7 +3,6 @@ import uuid
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
@@ -35,86 +34,21 @@ class Language(models.Model):
|
||||
verbose_name_plural = _('Sprachen')
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Model that holds a user's profile, including the django user model
|
||||
|
||||
The trust levels act as permission system and can be displayed as a badge for the user
|
||||
"""
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
# Moderators can make moderation decisions regarding the deletion of content
|
||||
# Coordinators can create adoption notices without them being checked
|
||||
# Members can create adoption notices that must be activated
|
||||
ADMIN = "admin"
|
||||
MODERATOR = "Moderator"
|
||||
COORDINATOR = "Koordinator*in"
|
||||
MEMBER = "Mitglied"
|
||||
TRUST_LEVEL = {
|
||||
ADMIN: 4,
|
||||
MODERATOR: 3,
|
||||
COORDINATOR: 2,
|
||||
MEMBER: 1,
|
||||
}
|
||||
|
||||
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Bevorzugte Sprache'))
|
||||
trust_level = models.IntegerField(choices=TRUST_LEVEL, default=TRUST_LEVEL[MEMBER])
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Nutzer*in')
|
||||
verbose_name_plural = _('Nutzer*innen')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("user-detail", args=[str(self.pk)])
|
||||
|
||||
def get_notifications_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def get_num_unread_notifications(self):
|
||||
return BaseNotification.objects.filter(user=self,read=False).count()
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return self
|
||||
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.alt_text
|
||||
|
||||
@property
|
||||
def as_html(self):
|
||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||
|
||||
|
||||
class Species(models.Model):
|
||||
"""Model representing a species of animal."""
|
||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||
verbose_name=_('Name'))
|
||||
|
||||
def __str__(self):
|
||||
"""String for representing the Model object."""
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Tierart')
|
||||
verbose_name_plural = _('Tierarten')
|
||||
|
||||
|
||||
class Location(models.Model):
|
||||
place_id = models.IntegerField()
|
||||
latitude = models.FloatField()
|
||||
longitude = models.FloatField()
|
||||
name = models.CharField(max_length=2000)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||
|
||||
@property
|
||||
def str_hr(self):
|
||||
return f"{self.name.split(',')[0]}"
|
||||
|
||||
@staticmethod
|
||||
def get_location_from_string(location_string):
|
||||
geo_api = geo.GeoAPI()
|
||||
@@ -134,6 +68,13 @@ class Location(models.Model):
|
||||
)
|
||||
return location
|
||||
|
||||
@staticmethod
|
||||
def add_location_to_object(instance):
|
||||
"""Search the location given in the location string and add it to the object"""
|
||||
location = Location.get_location_from_string(instance.location_string)
|
||||
instance.location = location
|
||||
instance.save()
|
||||
|
||||
|
||||
class RescueOrganization(models.Model):
|
||||
def __str__(self):
|
||||
@@ -155,13 +96,114 @@ class RescueOrganization(models.Model):
|
||||
|
||||
name = models.CharField(max_length=200)
|
||||
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
||||
allows_using_materials = models.CharField(max_length=200,default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED], choices=ALLOW_USE_MATERIALS_CHOICE, verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
||||
allows_using_materials = models.CharField(max_length=200,
|
||||
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
|
||||
choices=ALLOW_USE_MATERIALS_CHOICE,
|
||||
verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
||||
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
||||
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
||||
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
|
||||
fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil'))
|
||||
email = models.EmailField(null=True, blank=True, verbose_name=_('E-Mail'))
|
||||
phone_number = models.CharField(max_length=15, null=True, blank=True, verbose_name=_('Telefonnummer'))
|
||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("rescue-organization-detail", args=[str(self.pk)])
|
||||
|
||||
@property
|
||||
def adoption_notices(self):
|
||||
return AdoptionNotice.objects.filter(organization=self)
|
||||
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
# Moderators can make moderation decisions regarding the deletion of content
|
||||
# Coordinators can create adoption notices without them being checked
|
||||
# Members can create adoption notices that must be activated
|
||||
class TrustLevel(models.IntegerChoices):
|
||||
MEMBER = 1, 'Member'
|
||||
COORDINATOR = 2, 'Coordinator'
|
||||
MODERATOR = 3, 'Moderator'
|
||||
ADMIN = 4, 'Admin'
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Model that holds a user's profile, including the django user model
|
||||
|
||||
The trust levels act as permission system and can be displayed as a badge for the user
|
||||
"""
|
||||
|
||||
trust_level = models.IntegerField(
|
||||
choices=TrustLevel.choices,
|
||||
default=TrustLevel.MEMBER, # Default to the lowest trust level
|
||||
)
|
||||
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Bevorzugte Sprache'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Organisation'))
|
||||
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_(
|
||||
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
|
||||
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
|
||||
REQUIRED_FIELDS = ["reason_for_signup", "email"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Nutzer*in')
|
||||
verbose_name_plural = _('Nutzer*innen')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("user-detail", args=[str(self.pk)])
|
||||
|
||||
def get_notifications_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def get_num_unread_notifications(self):
|
||||
return BaseNotification.objects.filter(user=self, read=False).count()
|
||||
|
||||
@property
|
||||
def adoption_notices(self):
|
||||
return AdoptionNotice.objects.filter(owner=self)
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return self
|
||||
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.alt_text
|
||||
|
||||
@property
|
||||
def as_html(self):
|
||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||
|
||||
|
||||
class Species(models.Model):
|
||||
"""Model representing a species of animal."""
|
||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||
verbose_name=_('Name'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
"""String for representing the Model object."""
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Tierart')
|
||||
verbose_name_plural = _('Tierarten')
|
||||
|
||||
|
||||
class AdoptionNotice(models.Model):
|
||||
@@ -171,10 +213,13 @@ class AdoptionNotice(models.Model):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return self.name
|
||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
||||
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=datetime.now)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=datetime.now)
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||
@@ -191,6 +236,23 @@ class AdoptionNotice(models.Model):
|
||||
def animals(self):
|
||||
return Animal.objects.filter(adoption_notice=self)
|
||||
|
||||
@property
|
||||
def sexes(self):
|
||||
sexes = set()
|
||||
for animal in self.animals:
|
||||
sexes.update(animal.sex)
|
||||
return sexes
|
||||
|
||||
def sex_code(self):
|
||||
if len(self.sexes) > 1:
|
||||
return "mixed"
|
||||
elif self.sexes.pop() == Animal.MALE:
|
||||
return "male"
|
||||
elif self.sexes.pop() == Animal.FEMALE:
|
||||
return "female"
|
||||
else:
|
||||
return "mixed"
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return Comment.objects.filter(adoption_notice=self)
|
||||
@@ -276,15 +338,29 @@ class AdoptionNotice(models.Model):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_active
|
||||
|
||||
def set_checked(self):
|
||||
self.last_checked = datetime.now()
|
||||
self.save()
|
||||
|
||||
|
||||
@property
|
||||
def is_disabled_unchecked(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
||||
|
||||
def set_closed(self):
|
||||
self.last_checked = datetime.now()
|
||||
self.last_checked = timezone.now()
|
||||
self.adoptionnoticestatus.set_closed()
|
||||
|
||||
def set_active(self):
|
||||
self.last_checked = timezone.now()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_active()
|
||||
|
||||
def set_unchecked(self):
|
||||
self.last_checked = timezone.now()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_unchecked()
|
||||
|
||||
|
||||
class AdoptionNoticeStatus(models.Model):
|
||||
"""
|
||||
@@ -324,6 +400,7 @@ class AdoptionNoticeStatus(models.Model):
|
||||
"against_the_rules": "against_the_rules",
|
||||
"missing_information": "missing_information",
|
||||
"technical_error": "technical_error",
|
||||
"unchecked": "unchecked",
|
||||
"other": "other"
|
||||
}
|
||||
}
|
||||
@@ -334,23 +411,51 @@ class AdoptionNoticeStatus(models.Model):
|
||||
minor_choices.update(MINOR_STATUS_CHOICES[key])
|
||||
minor_status = models.CharField(choices=minor_choices, max_length=200)
|
||||
adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
|
||||
|
||||
def as_string(self):
|
||||
return f"{self.major_status}, {self.minor_status}"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.major_status == self.ACTIVE
|
||||
|
||||
@property
|
||||
def is_disabled_unchecked(self):
|
||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||
|
||||
@staticmethod
|
||||
def get_minor_choices(major_status):
|
||||
return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
|
||||
|
||||
@staticmethod
|
||||
def create_other(an_instance):
|
||||
# Used as empty status to be changed immediately
|
||||
major_status = AdoptionNoticeStatus.DISABLED
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.DISABLED]["other"]
|
||||
AdoptionNoticeStatus.objects.create(major_status=major_status,
|
||||
minor_status=minor_status,
|
||||
adoption_notice=an_instance)
|
||||
|
||||
def set_closed(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||
self.save()
|
||||
|
||||
def set_unchecked(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
|
||||
self.save()
|
||||
|
||||
def set_active(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.ACTIVE]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.ACTIVE]["searching"]
|
||||
self.save()
|
||||
|
||||
|
||||
class Animal(models.Model):
|
||||
MALE_NEUTERED = "M_N"
|
||||
@@ -372,13 +477,15 @@ class Animal(models.Model):
|
||||
sex = models.CharField(max_length=20, choices=SEX_CHOICES, )
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
return datetime.today().date() - self.date_of_birth
|
||||
return timezone.now().today().date() - self.date_of_birth
|
||||
|
||||
@property
|
||||
def hr_age(self):
|
||||
@@ -415,6 +522,8 @@ class Rule(models.Model):
|
||||
language = models.ForeignKey(Language, on_delete=models.PROTECT)
|
||||
# Rule identifier allows to translate rules with the same identifier
|
||||
rule_identifier = models.CharField(max_length=24)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -438,6 +547,7 @@ class Report(models.Model):
|
||||
reported_broken_rules = models.ManyToManyField(Rule)
|
||||
user_comment = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.status}]: {self.user_comment:.20}"
|
||||
@@ -484,13 +594,12 @@ class ModerationAction(models.Model):
|
||||
}
|
||||
action = models.CharField(max_length=30, choices=ACTIONS.items())
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
public_comment = models.TextField(blank=True)
|
||||
# Only visible to moderator
|
||||
private_comment = models.TextField(blank=True)
|
||||
report = models.ForeignKey(Report, on_delete=models.CASCADE)
|
||||
|
||||
# TODO: Needs field for moderator that performed the action
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.action}]: {self.public_comment}"
|
||||
|
||||
@@ -516,6 +625,17 @@ class Text(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.language})"
|
||||
|
||||
@staticmethod
|
||||
def get_texts(text_codes, language, expandable_dict=None):
|
||||
if expandable_dict is None:
|
||||
expandable_dict = {}
|
||||
for text_code in text_codes:
|
||||
try:
|
||||
expandable_dict[text_code] = Text.objects.get(text_code=text_code, language=language, )
|
||||
except Text.DoesNotExist:
|
||||
expandable_dict[text_code] = None
|
||||
return expandable_dict
|
||||
|
||||
|
||||
class Announcement(Text):
|
||||
"""
|
||||
@@ -523,7 +643,8 @@ class Announcement(Text):
|
||||
"""
|
||||
logged_in_only = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunk")
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
|
||||
publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
|
||||
IMPORTANT = "important"
|
||||
WARNING = "warning"
|
||||
@@ -571,6 +692,7 @@ class Comment(models.Model):
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
|
||||
@@ -588,6 +710,7 @@ class Comment(models.Model):
|
||||
|
||||
class BaseNotification(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
title = models.CharField(max_length=100)
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
@@ -605,7 +728,6 @@ class CommentNotification(BaseNotification):
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
print(f"URL: self.comment.get_absolute_url()")
|
||||
return self.comment.get_absolute_url
|
||||
|
||||
|
||||
@@ -613,6 +735,33 @@ class Subscriptions(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.owner} - { self.adoption_notice }"
|
||||
return f"{self.owner} - {self.adoption_notice}"
|
||||
|
||||
|
||||
class Log(models.Model):
|
||||
"""
|
||||
Basic class that allows logging random entries for later inspection
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"), blank=True, null=True)
|
||||
action = models.CharField(max_length=255, verbose_name=_("Aktion"))
|
||||
text = models.CharField(max_length=1000, verbose_name=_("Log text"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.action}] - {self.user} - {self.created_at.strftime('%H:%M:%S %d-%m-%Y ')}"
|
||||
|
||||
|
||||
class Timestamp(models.Model):
|
||||
"""
|
||||
Class to store timestamps based on keys
|
||||
"""
|
||||
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
|
||||
data = models.CharField(max_length=2000, blank=True, null=True)
|
||||
|
||||
def ___str__(self):
|
||||
return f"[{self.key}] - {self.timestamp.strftime('%H:%M:%S %d-%m-%Y ')} - {self.data}"
|
||||
|
37
src/fellchensammlung/receivers.py
Normal file
37
src/fellchensammlung/receivers.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
|
||||
from .tasks import task_send_notification_email
|
||||
from notfellchen.settings import host
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@receiver(post_save, sender=CommentNotification)
|
||||
def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
||||
base_notification_receiver(sender, instance, created, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_save, sender=BaseNotification)
|
||||
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
||||
if not created or not instance.user.email_notifications:
|
||||
return
|
||||
else:
|
||||
task_send_notification_email.delay(instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def notification_new_user(sender, instance: User, created: bool, **kwargs):
|
||||
NEWLINE = "\r\n"
|
||||
if not created:
|
||||
return
|
||||
# Create Notification text
|
||||
subject = _("Neuer User") + f": {instance.username}"
|
||||
new_user_text = _("Es hat sich eine neue Person registriert.") + f"{NEWLINE}"
|
||||
user_detail_text = _("Username") + f": {instance.username}{NEWLINE}" + _(
|
||||
"E-Mail") + f": {instance.email}{NEWLINE}"
|
||||
user_url = "https://" + host + instance.get_absolute_url()
|
||||
link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
|
||||
body_text = new_user_text + user_detail_text + link_text
|
||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||
notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator)
|
||||
notification.save()
|
@@ -1,3 +1,7 @@
|
||||
/***************/
|
||||
/* MAIN COLORS */
|
||||
/***************/
|
||||
|
||||
:root {
|
||||
--primary-light-one: #5daa68;
|
||||
--primary-light-two: #4a9455;
|
||||
@@ -18,7 +22,9 @@
|
||||
--text-three: var(--primary-light-one);
|
||||
--shadow-three: var(--primary-dark-one);
|
||||
}
|
||||
|
||||
/**************************/
|
||||
/* TAG SETTINGS (GENERAL) */
|
||||
/**************************/
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
@@ -30,10 +36,6 @@ body {
|
||||
}
|
||||
|
||||
|
||||
.content-box {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -79,6 +81,146 @@ h1, h2 {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
|
||||
/**************/
|
||||
/* CONTAINERS */
|
||||
/**************/
|
||||
|
||||
.container-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1 25%;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
background: var(--background-three);
|
||||
color: var(--text-two);
|
||||
}
|
||||
|
||||
.container-edit-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
/* PARTIAL SPECIFIC CONTAINERS */
|
||||
/*******************************/
|
||||
|
||||
|
||||
.detail-animal-header {
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.detail-animal-header {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
background-color: var(--highlight-two);
|
||||
color: var(--highlight-one-text);
|
||||
|
||||
.btn2 {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.button_darken:hover {
|
||||
background-color: var(--highlight-one);
|
||||
color: var(--highlight-one-text);
|
||||
}
|
||||
|
||||
button {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.container-comment-form {
|
||||
width: 80%;
|
||||
color: var(--text-one);
|
||||
|
||||
b {
|
||||
text-shadow: 2px 2px var(--shadow-one);
|
||||
}
|
||||
}
|
||||
|
||||
/*************/
|
||||
/* Modifiers */
|
||||
/*************/
|
||||
|
||||
/* Used to enlargen cards */
|
||||
.full-width {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/***********/
|
||||
/* BUTTONS */
|
||||
/***********/
|
||||
|
||||
select, .button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
background-color: var(--secondary-light-one);
|
||||
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-light-one);
|
||||
color: var(--secondary-light-one);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn2 {
|
||||
background-color: var(--secondary-light-one);
|
||||
color: var(--primary-dark-one);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*********************/
|
||||
/* UNIQUE COMPONENTS */
|
||||
/*********************/
|
||||
|
||||
.content-box {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
overflow: hidden;
|
||||
background-color: var(--background-two);
|
||||
@@ -110,14 +252,7 @@ h1, h2 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
select, .button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
background-color: var(--secondary-light-one);
|
||||
|
||||
}
|
||||
|
||||
.header-right select.option {
|
||||
color: #000;
|
||||
@@ -135,26 +270,6 @@ select, .button {
|
||||
height: 67px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
background-color: var(--highlight-two);
|
||||
color: var(--highlight-one-text);
|
||||
|
||||
.btn2 {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.button_darken:hover {
|
||||
background-color: var(--highlight-one);
|
||||
color: var(--highlight-one-text);
|
||||
}
|
||||
|
||||
button {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.header a {
|
||||
@@ -172,25 +287,6 @@ select, .button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
background-color: var(--primary-light-one);
|
||||
color: var(--secondary-light-one);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn2 {
|
||||
background-color: var(--secondary-light-one);
|
||||
color: var(--primary-dark-one);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.form-button, .link-button a:link, .link-button a:visited {
|
||||
background-color: #4ba3cd;
|
||||
color: white;
|
||||
@@ -203,14 +299,6 @@ select, .button {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.container-edit-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-button:hover, .link-button a:hover, .link-button a:active {
|
||||
background-color: #4090b6;
|
||||
@@ -326,18 +414,6 @@ select, .button {
|
||||
}
|
||||
}
|
||||
|
||||
.detail-animal-header {
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.detail-animal-header {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
border: black 1px solid;
|
||||
@@ -355,10 +431,7 @@ select, .button {
|
||||
}
|
||||
|
||||
|
||||
.container-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.photos {
|
||||
@@ -376,14 +449,6 @@ select, .button {
|
||||
border-radius: 10%;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1 25%;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
background: var(--background-three);
|
||||
color: var(--text-two);
|
||||
}
|
||||
|
||||
.card h1 {
|
||||
color: var(--text-three);
|
||||
@@ -514,20 +579,7 @@ select, .button {
|
||||
color: var(--text-two);
|
||||
}
|
||||
|
||||
.container-comment-form {
|
||||
width: 80%;
|
||||
color: var(--text-one);
|
||||
|
||||
b {
|
||||
text-shadow: 2px 2px var(--shadow-one);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.form-comments {
|
||||
.btn {
|
||||
@@ -552,6 +604,21 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-search {
|
||||
select, input {
|
||||
background-color: var(--primary-light-one);
|
||||
color: var(--text-one);
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/************************/
|
||||
/* GENERAL HIGHLIGHTING */
|
||||
/************************/
|
||||
|
||||
.important {
|
||||
border: #e01137 4px solid;
|
||||
}
|
||||
@@ -564,15 +631,10 @@ textarea {
|
||||
border: rgba(17, 58, 224, 0.51) 4px solid;
|
||||
}
|
||||
|
||||
.form-search {
|
||||
select, input {
|
||||
background-color: var(--primary-light-one);
|
||||
color: var(--text-one);
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
}
|
||||
/*******/
|
||||
/* MAP */
|
||||
/*******/
|
||||
|
||||
.marker {
|
||||
background-image: url('../img/logo_transparent.png');
|
||||
@@ -594,4 +656,4 @@ textarea {
|
||||
.map-in-content #map {
|
||||
height: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
52
src/fellchensammlung/tasks.py
Normal file
52
src/fellchensammlung/tasks.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from celery.app import shared_task
|
||||
from django.utils import timezone
|
||||
from notfellchen.celery import app as celery_app
|
||||
from .mail import send_notification_email
|
||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
||||
from .tools.misc import healthcheck_ok
|
||||
from .models import Location, AdoptionNotice, Timestamp
|
||||
|
||||
|
||||
def set_timestamp(key: str):
|
||||
try:
|
||||
ts = Timestamp.objects.get(key=key)
|
||||
ts.timestamp = timezone.now()
|
||||
ts.save()
|
||||
except Timestamp.DoesNotExist:
|
||||
Timestamp.objects.create(key=key, timestamp=timezone.now())
|
||||
|
||||
|
||||
@celery_app.task(name="admin.clean_locations")
|
||||
def task_clean_locations():
|
||||
clean_locations()
|
||||
set_timestamp("task_clean_locations")
|
||||
|
||||
|
||||
@celery_app.task(name="admin.daily_unchecked_deactivation")
|
||||
def task_deactivate_unchecked():
|
||||
deactivate_unchecked_adoption_notices()
|
||||
set_timestamp("task_daily_unchecked_deactivation")
|
||||
|
||||
|
||||
@celery_app.task(name="admin.deactivate_404_adoption_notices")
|
||||
def task_deactivate_unchecked():
|
||||
deactivate_404_adoption_notices()
|
||||
set_timestamp("task_deactivate_404_adoption_notices")
|
||||
|
||||
|
||||
@celery_app.task(name="commit.add_location")
|
||||
def add_adoption_notice_location(pk):
|
||||
instance = AdoptionNotice.objects.get(pk=pk)
|
||||
Location.add_location_to_object(instance)
|
||||
set_timestamp("add_adoption_notice_location")
|
||||
|
||||
|
||||
@celery_app.task(name="tools.healthcheck")
|
||||
def task_healthcheck():
|
||||
healthcheck_ok()
|
||||
set_timestamp("task_healthcheck")
|
||||
|
||||
|
||||
@shared_task
|
||||
def task_send_notification_email(notification_pk):
|
||||
send_notification_email(notification_pk)
|
@@ -2,7 +2,14 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if about_us %}
|
||||
<h1>{{ about_us.title }}</h1>
|
||||
{{ about_us.content | render_markdown }}
|
||||
{% endif %}
|
||||
|
||||
<h1>{% translate "Regeln" %}</h1>
|
||||
{% include "fellchensammlung/lists/list-rules.html" %}
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block title %}<title>Notfellchen</title>{% endblock %}
|
||||
{% block title %}{% endblock %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{% translate "Farbratten aus dem Tierschutz finden und adoptieren" %}">
|
||||
<!-- Add additional CSS in static file -->
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">
|
||||
|
@@ -0,0 +1,29 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ org.name }}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>{{ org.name }}</h1>
|
||||
|
||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||
{% if org.location %}
|
||||
{{ org.location.str_hr }}
|
||||
{% else %}
|
||||
{{ org.location_string }}
|
||||
{% endif %}
|
||||
<p>{{ org.description | render_markdown }}</p>
|
||||
</div>
|
||||
<h2>{% translate 'Vermittlungen der Organisation' %}</h2>
|
||||
<div class="container-cards">
|
||||
{% if org.adoption_notices %}
|
||||
{% for adoption_notice in org.adoption_notices %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@@ -13,12 +13,38 @@
|
||||
<p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if user.id is request.user.id %}
|
||||
<div class="container-cards">
|
||||
{% if user.id is request.user.id %}
|
||||
<div class="card">
|
||||
{% if token %}
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
|
||||
<input class="btn" type="submit" name="delete_token"
|
||||
value={% translate "Delete API token" %}>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% translate "Kein API-Token vorhanden." %}</p>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input class="btn" type="submit" name="create_token"
|
||||
value={% translate "Create API token" %}>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div><p>
|
||||
<div class="container-comment-form">
|
||||
<h2>{% trans 'Profil verwalten' %}</h2>
|
||||
<p>
|
||||
<a class="btn2" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
|
||||
<a class="btn2" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
|
||||
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -2,23 +2,25 @@
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="detail-adoption-notice-header">
|
||||
<h1 class="detail-adoption-notice-header">{{ adoption_notice.name }}
|
||||
{% if not is_subscribed %}
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="subscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
|
||||
</form>
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="subscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unsubscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
|
||||
</form>
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unsubscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if has_edit_permission %}
|
||||
@@ -32,6 +34,9 @@
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% translate "Ort" %}</th>
|
||||
{% if adoption_notice.organization %}
|
||||
<th>{% translate "Organisation" %}</th>
|
||||
{% endif %}
|
||||
<th>{% translate "Suchen seit" %}</th>
|
||||
<th>{% translate "Zuletzt aktualisiert" %}</th>
|
||||
<th>{% translate "Weitere Informationen" %}</th>
|
||||
@@ -44,11 +49,22 @@
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if adoption_notice.organization %}
|
||||
<td><a href="{{adoption_notice.organization.get_absolute_url }}">{{ adoption_notice.organization }}</a></td>
|
||||
{% endif %}
|
||||
|
||||
<td>{{ adoption_notice.searching_since }}</td>
|
||||
<td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td>
|
||||
{% if adoption_notice.further_information %}
|
||||
<td>{{ adoption_notice.link_to_more_information | safe }}</td>
|
||||
<td>
|
||||
<form method="get" action="{% url 'external-site' %}">
|
||||
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
|
||||
<button class="btn" type="submit" id="submit">
|
||||
{{ adoption_notice.further_information | domain }} <i
|
||||
class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>-</td>
|
||||
{% endif %}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ animal.name }}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "fellchensammlung/details/detail-animal-partial.html" %}
|
||||
{% endblock %}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>403 Forbidden</h1>
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst, dass hier
|
||||
ein Fehler vorliegt, kontaktiere das Team!
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
{% endblock %}
|
@@ -0,0 +1,15 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
{% if external_site_warning %}
|
||||
{{ external_site_warning.content | render_markdown }}
|
||||
{% else %}
|
||||
{% blocktranslate %}
|
||||
<p>Achtung du verlässt notfellchen.org</p>
|
||||
{% endblocktranslate %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}" class="btn button">{% translate "Weiter" %}</a>
|
||||
</div>
|
||||
{% endblock content %}
|
@@ -8,9 +8,6 @@
|
||||
<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE_CURRENT %} selected{% endif %}>
|
||||
{{ language.0|language_name_local }}
|
||||
</option>
|
||||
<!--<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE %} selected{% endif %}>
|
||||
{{ language.0|language_name_local }} ({{ language.0 }})
|
||||
</option>-->
|
||||
{% endfor %}
|
||||
</select>
|
||||
<!--<input type="submit" value={% translate "change" %}>-->
|
||||
|
@@ -2,6 +2,8 @@
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% translate "Vermitteln" %}</h1>
|
||||
<p>
|
||||
|
@@ -26,7 +26,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<a class="btn2" href="{{ user.get_absolute_url }}"><i aria-hidden="true" class="fas fa-user"></i></a>
|
||||
<a class="btn2" href="{% url 'user-me' %}"><i aria-hidden="true" class="fas fa-user"></i></a>
|
||||
<form class="btn2 button_darken" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>
|
||||
|
@@ -2,6 +2,8 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% for announcement in announcements %}
|
||||
{% include "fellchensammlung/partials/partial-announcement.html" %}
|
||||
|
@@ -1,28 +1,51 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}<title>{% translate "Instanz-Check" %}</title> {% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>{% translate "Instanz-Check" %}</h1>
|
||||
{% if missing_texts|length > 0 %}
|
||||
<h2>{% trans "Fehlende Texte" %}</h2>
|
||||
<p>
|
||||
<table>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% translate "Text Code" %}</th>
|
||||
<th>{% translate "Sprache" %}</th>
|
||||
</tr>
|
||||
{% for missing_text in missing_texts %}
|
||||
<tr>
|
||||
<th>{% translate "Text Code" %}</th>
|
||||
<th>{% translate "Sprache" %}</th>
|
||||
<td>{{ missing_text.0 }}</td>
|
||||
<td>{{ missing_text.1 }}</td>
|
||||
</tr>
|
||||
{% for missing_text in missing_texts %}
|
||||
<tr>
|
||||
<td>{{ missing_text.0 }}</td>
|
||||
<td>{{ missing_text.1 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>{% translate "Texte scheinen vollständig" %}</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% trans "Zeitstempel" %}</h2>
|
||||
{% if timestamps|length > 0 %}
|
||||
<p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% translate "Key" %}</th>
|
||||
<th>{% translate "Zeitstempel" %}</th>
|
||||
<th>{% translate "Daten" %}</th>
|
||||
</tr>
|
||||
{% for timestamp in timestamps %}
|
||||
<tr>
|
||||
<td>{{ timestamp.key }}</td>
|
||||
<td>{{ timestamp.timestamp }}</td>
|
||||
<td>{{ timestamp.data }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>{% translate "Keine Zeitstempel geloggt." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% translate "Nicht-lokalisierte Vermittlungen" %}</h2>
|
||||
{% if number_not_geocoded_adoption_notices > 0 %}
|
||||
<details>
|
||||
@@ -55,6 +78,22 @@
|
||||
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% translate "Nicht-geprüfte Vermittlungen" %}</h2>
|
||||
{% if number_unchecked_ans > 0 %}
|
||||
<details>
|
||||
<summary>{{ number_unchecked_ans }}</summary>
|
||||
<ul>
|
||||
{% for unchecked_an in unchecked_ans %}
|
||||
<li>
|
||||
<a href="{{ unchecked_an.get_absolute_url }}">{{ unchecked_an.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% else %}
|
||||
<p>{{ number_unchecked_ans }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="clean_locations">
|
||||
@@ -62,5 +101,13 @@
|
||||
<i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="deactivate_unchecked_adoption_notices">
|
||||
<button class="btn" type="submit" id="submit">
|
||||
<i class="fa-solid fa-broom"></i> {% translate "Deaktivire ungeprüfte Vermittlungen" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="container-cards">
|
||||
{% if adoption_notices %}
|
||||
{% for adoption_notice in adoption_notices %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice.html" %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}<title>{% translate "Karte" %}</title> %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
|
@@ -1,5 +1,6 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}<title>{% translate "Modqueue" %}</title> %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% translate "Modqueue" %}</h1>
|
||||
|
@@ -10,9 +10,9 @@
|
||||
class="fa-solid fa-flag"></i></a>
|
||||
</div>
|
||||
<p>
|
||||
<b>Ort</b>
|
||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{{ adoption_notice.location.str_hr }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<p>
|
||||
<b>Ort</b>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{{ adoption_notice.location.str_hr }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||
</h1>
|
||||
{% if adoption_notice.further_information %}
|
||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_inactive">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung inaktiv" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@@ -1,5 +1,8 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form class="form-search card" method="post">
|
||||
{% csrf_token %}
|
||||
|
@@ -3,32 +3,16 @@
|
||||
{% block content %}
|
||||
<h1>{% translate "Aktualitätscheck" %}</h1>
|
||||
<p>{% translate "Überprüfe ob Vermittlungen noch aktuell sind" %}</p>
|
||||
{% for adoption_notice in adoption_notices %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||
</h1>
|
||||
{% if adoption_notice.further_information %}
|
||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_inactive">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung inaktiv" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Deaktivierte Vermittlungen zur Überprüfung' %}</h1>
|
||||
{% for adoption_notice in adoption_notices_disabled %}
|
||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Aktive Vermittlungen zur Überprüfung' %}</h1>
|
||||
{% for adoption_notice in adoption_notices_active %}
|
||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -4,6 +4,7 @@ from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils.safestring import mark_safe
|
||||
from notfellchen import settings
|
||||
from urllib.parse import urlparse
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -56,6 +57,17 @@ def pointdecimal(value):
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
@register.filter
|
||||
@stringfilter
|
||||
def domain(url):
|
||||
try:
|
||||
domain = urlparse(url).netloc
|
||||
if domain.startswith("www."):
|
||||
return domain[4:]
|
||||
return domain
|
||||
except ValueError:
|
||||
return url
|
||||
|
||||
@register.simple_tag
|
||||
def settings_value(name):
|
||||
return getattr(settings, name)
|
||||
|
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@@ -1,4 +1,11 @@
|
||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log
|
||||
from fellchensammlung.tools.misc import is_404
|
||||
|
||||
|
||||
def clean_locations(quiet=True):
|
||||
# ADOPTION NOTICES
|
||||
@@ -7,7 +14,7 @@ def clean_locations(quiet=True):
|
||||
num_without_location = adoption_notices_without_location.count()
|
||||
if not quiet:
|
||||
print(f"From {num_of_all} there are {num_without_location} adoption notices without location "
|
||||
f"({num_without_location/num_of_all*100:.2f}%)")
|
||||
f"({num_without_location / num_of_all * 100:.2f}%)")
|
||||
for adoption_notice in adoption_notices_without_location:
|
||||
if not quiet:
|
||||
print(f"Searching {adoption_notice.location_string} in Nominatim")
|
||||
@@ -28,7 +35,7 @@ def clean_locations(quiet=True):
|
||||
num_without_location = rescue_orgs_without_location.count()
|
||||
if not quiet:
|
||||
print(f"From {num_of_all} there are {num_without_location} adoption notices without location "
|
||||
f"({num_without_location/num_of_all*100:.2f}%)")
|
||||
f"({num_without_location / num_of_all * 100:.2f}%)")
|
||||
for rescue_org in rescue_orgs_without_location:
|
||||
if not quiet:
|
||||
print(f"Searching {rescue_org.location_string} in Nominatim")
|
||||
@@ -41,4 +48,37 @@ def clean_locations(quiet=True):
|
||||
num_without_location_new = rescue_orgs_without_location_new.count()
|
||||
num_new = num_without_location - num_without_location_new
|
||||
if not quiet:
|
||||
print(f"Added {num_new} new locations")
|
||||
print(f"Added {num_new} new locations")
|
||||
|
||||
|
||||
def get_unchecked_adoption_notices(weeks=3):
|
||||
now = timezone.now()
|
||||
three_weeks_ago = now - timedelta(weeks=weeks)
|
||||
|
||||
# Query for active adoption notices that were checked in the last three weeks
|
||||
unchecked_adoptions = AdoptionNotice.objects.filter(
|
||||
last_checked__lte=three_weeks_ago
|
||||
)
|
||||
active_unchecked_adoptions = [adoption for adoption in unchecked_adoptions if adoption.is_active]
|
||||
return active_unchecked_adoptions
|
||||
|
||||
|
||||
def get_active_adoption_notices():
|
||||
ans = AdoptionNotice.objects.all()
|
||||
active_adoptions = [adoption for adoption in ans if adoption.is_active]
|
||||
return active_adoptions
|
||||
|
||||
|
||||
def deactivate_unchecked_adoption_notices():
|
||||
for adoption_notice in get_unchecked_adoption_notices(weeks=3):
|
||||
AdoptionNoticeStatus.objects.get(adoption_notice=adoption_notice).set_unchecked()
|
||||
|
||||
|
||||
def deactivate_404_adoption_notices():
|
||||
for adoption_notice in get_active_adoption_notices():
|
||||
if adoption_notice.further_information and adoption_notice.further_information != "":
|
||||
if is_404(adoption_notice.further_information):
|
||||
adoption_notice.set_closed()
|
||||
logging_msg = f"Automatically set Adoption Notice {adoption_notice.id} closed as link to more information returened 404"
|
||||
logging.info(logging_msg)
|
||||
Log.objects.create(action="automated", text=logging_msg)
|
||||
|
@@ -7,7 +7,6 @@ from notfellchen import __version__ as nf_version
|
||||
from notfellchen import settings
|
||||
|
||||
|
||||
|
||||
def calculate_distance_between_coordinates(position1, position2):
|
||||
"""
|
||||
Calculate the distance between two points identified by coordinates
|
||||
@@ -65,7 +64,8 @@ class GeoAPI:
|
||||
|
||||
def get_coordinates_from_query(self, location_string):
|
||||
try:
|
||||
result = self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0]
|
||||
result = \
|
||||
self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0]
|
||||
except IndexError:
|
||||
return None
|
||||
return result["lat"], result["lon"]
|
||||
@@ -77,9 +77,9 @@ class GeoAPI:
|
||||
def get_geojson_for_query(self, location_string):
|
||||
try:
|
||||
result = self.requests.get(self.api_url,
|
||||
{"q": location_string,
|
||||
"format": "jsonv2"},
|
||||
headers=self.headers).json()
|
||||
{"q": location_string,
|
||||
"format": "jsonv2"},
|
||||
headers=self.headers).json()
|
||||
except Exception as e:
|
||||
logging.warning(f"Exception {e} when querying Nominatim")
|
||||
return None
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import datetime as datetime
|
||||
import logging
|
||||
|
||||
from notfellchen import settings
|
||||
import requests
|
||||
|
||||
|
||||
def pluralize(number, letter="e"):
|
||||
@@ -11,11 +15,11 @@ def pluralize(number, letter="e"):
|
||||
|
||||
def age_as_hr_string(age: datetime.timedelta) -> str:
|
||||
days = age.days
|
||||
weeks = age.days/7
|
||||
months = age.days/30
|
||||
years = age.days/365
|
||||
weeks = age.days / 7
|
||||
months = age.days / 30
|
||||
years = age.days / 365
|
||||
if years >= 1:
|
||||
months = months - 12*years
|
||||
months = months - 12 * years
|
||||
return f'{years:.0f} Jahr{pluralize(years)} und {months:.0f} Monat{pluralize(months)}'
|
||||
elif months >= 3:
|
||||
return f'{months:.0f} Monat{pluralize(months)}'
|
||||
@@ -23,3 +27,18 @@ def age_as_hr_string(age: datetime.timedelta) -> str:
|
||||
return f'{weeks:.0f} Woche{pluralize(weeks, "n")}'
|
||||
else:
|
||||
return f'{days:.0f} Tag{pluralize(days)}'
|
||||
|
||||
|
||||
def healthcheck_ok():
|
||||
try:
|
||||
requests.get(settings.HEALTHCHECKS_URL, timeout=10)
|
||||
except requests.RequestException as e:
|
||||
logging.error("Ping to healthcheck-server failed: %s" % e)
|
||||
|
||||
|
||||
def is_404(url):
|
||||
try:
|
||||
result = requests.get(url, timeout=10)
|
||||
return result.status_code == 404
|
||||
except requests.RequestException as e:
|
||||
logging.warning(f"Request to {url} failed: {e}")
|
||||
|
@@ -24,6 +24,8 @@ urlpatterns = [
|
||||
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, name="adoption-notice-add-photo"),
|
||||
# ex: /adoption_notice/2/add-animal
|
||||
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, name="adoption-notice-add-animal"),
|
||||
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
||||
name="rescue-organization-detail"),
|
||||
|
||||
# ex: /search/
|
||||
path("suchen/", views.search, name="search"),
|
||||
@@ -50,7 +52,9 @@ urlpatterns = [
|
||||
## USERS ##
|
||||
###########
|
||||
# ex: user/1
|
||||
path("user/<int:user_id>/", views.user_detail, name="user-detail"),
|
||||
path("user/<int:user_id>/", views.user_by_id, name="user-detail"),
|
||||
path("user/me/", views.my_profile, name="user-me"),
|
||||
path('user/me/export/', views.export_own_profile, name='user-me-export'),
|
||||
|
||||
path('accounts/register/',
|
||||
RegistrationView.as_view(
|
||||
@@ -79,5 +83,9 @@ urlpatterns = [
|
||||
#########
|
||||
path('api/', include('fellchensammlung.api.urls')),
|
||||
|
||||
###################
|
||||
## External Site ##
|
||||
###################
|
||||
path('external-site/', views.external_site_warning, name="external-site"),
|
||||
|
||||
]
|
||||
|
@@ -1,38 +1,43 @@
|
||||
import logging
|
||||
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils import translation
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.serializers import serialize
|
||||
import json
|
||||
|
||||
from .mail import mail_admins_new_report
|
||||
from notfellchen import settings
|
||||
|
||||
from fellchensammlung import logger
|
||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, Species
|
||||
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
||||
Species, Log, Timestamp, TrustLevel
|
||||
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
|
||||
CommentForm, ReportCommentForm, AnimalForm, \
|
||||
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
|
||||
from .models import Language, Announcement
|
||||
from .tools.geo import GeoAPI
|
||||
from .tools.metrics import gather_metrics_data
|
||||
from .tools.admin import clean_locations
|
||||
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices
|
||||
from .tasks import add_adoption_notice_location
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
|
||||
def user_is_trust_level_or_above(user, trust_level=User.MODERATOR):
|
||||
return user.is_authenticated and user.trust_level >= User.TRUST_LEVEL[trust_level]
|
||||
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
|
||||
return user.is_authenticated and user.trust_level >= trust_level
|
||||
|
||||
|
||||
def user_is_owner_or_trust_level(user, django_object, trust_level=User.MODERATOR):
|
||||
def user_is_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
||||
return user.is_authenticated and (
|
||||
user.trust_level == User.TRUST_LEVEL[trust_level] or django_object.owner == user)
|
||||
user.trust_level == trust_level or django_object.owner == user)
|
||||
|
||||
|
||||
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=User.MODERATOR):
|
||||
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
||||
if not user_is_owner_or_trust_level(user, django_object, trust_level):
|
||||
raise PermissionDenied
|
||||
|
||||
@@ -46,12 +51,9 @@ def index(request):
|
||||
lang = Language.objects.get(languagecode=language_code)
|
||||
active_announcements = Announcement.get_active_announcements(lang)
|
||||
|
||||
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions, "announcements": active_announcements}
|
||||
for text_code in ["how_to", "introduction"]:
|
||||
try:
|
||||
context[text_code] = Text.objects.get(text_code=text_code, language=lang, )
|
||||
except Text.DoesNotExist:
|
||||
context[text_code] = None
|
||||
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions,
|
||||
"announcements": active_announcements}
|
||||
Text.get_texts(["how_to", "introduction"], lang, context)
|
||||
|
||||
return render(request, 'fellchensammlung/index.html', context=context)
|
||||
|
||||
@@ -70,17 +72,21 @@ def change_language(request):
|
||||
translation.activate(language_code)
|
||||
response = HttpResponseRedirect(redirect_path)
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language_code)
|
||||
return response
|
||||
return response
|
||||
else:
|
||||
return render(request, 'fellchensammlung/index.html')
|
||||
|
||||
|
||||
def adoption_notice_detail(request, adoption_notice_id):
|
||||
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
|
||||
if request.user.is_authenticated:
|
||||
try:
|
||||
try:
|
||||
subscription = Subscriptions.objects.get(owner=request.user, adoption_notice=adoption_notice)
|
||||
is_subscribed = True
|
||||
except Subscriptions.DoesNotExist:
|
||||
is_subscribed = False
|
||||
else:
|
||||
is_subscribed = False
|
||||
has_edit_permission = user_is_owner_or_trust_level(request.user, adoption_notice)
|
||||
if request.method == 'POST':
|
||||
action = request.POST.get("action")
|
||||
@@ -94,6 +100,10 @@ def adoption_notice_detail(request, adoption_notice_id):
|
||||
comment_instance.user = request.user
|
||||
comment_instance.save()
|
||||
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="comment",
|
||||
text=f"{request.user} hat Kommentar {comment_instance.pk} zur Vermittlung {adoption_notice_id} hinzugefügt")
|
||||
|
||||
# Auto-subscribe user to adoption notice
|
||||
subscription, created = Subscriptions.objects.get_or_create(adoption_notice=adoption_notice,
|
||||
owner=request.user)
|
||||
@@ -104,9 +114,9 @@ def adoption_notice_detail(request, adoption_notice_id):
|
||||
# Create a notification but only if the user is not the one that posted the comment
|
||||
if subscription.owner != request.user:
|
||||
notification = CommentNotification(user=subscription.owner,
|
||||
title=f"{adoption_notice.name} - Neuer Kommentar",
|
||||
text=f"{request.user}: {comment_instance.text}",
|
||||
comment=comment_instance)
|
||||
title=f"{adoption_notice.name} - Neuer Kommentar",
|
||||
text=f"{request.user}: {comment_instance.text}",
|
||||
comment=comment_instance)
|
||||
notification.save()
|
||||
else:
|
||||
comment_form = CommentForm(instance=adoption_notice)
|
||||
@@ -114,13 +124,13 @@ def adoption_notice_detail(request, adoption_notice_id):
|
||||
Subscriptions.objects.create(owner=request.user, adoption_notice=adoption_notice)
|
||||
is_subscribed = True
|
||||
if action == "unsubscribe":
|
||||
subscription.delete()
|
||||
is_subscribed = False
|
||||
subscription.delete()
|
||||
is_subscribed = False
|
||||
else:
|
||||
raise PermissionDenied
|
||||
else:
|
||||
comment_form = CommentForm(instance=adoption_notice)
|
||||
context = {"adoption_notice": adoption_notice,"comment_form": comment_form, "user": request.user,
|
||||
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
|
||||
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
|
||||
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
|
||||
|
||||
@@ -141,6 +151,10 @@ def adoption_notice_edit(request, adoption_notice_id):
|
||||
location = Location.get_location_from_string(adoption_notice_instance.location_string)
|
||||
adoption_notice_instance.location = location
|
||||
adoption_notice_instance.save()
|
||||
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="adoption_notice_edit",
|
||||
text=f"{request.user} hat Vermittlung {adoption_notice.pk} geändert")
|
||||
return redirect(reverse("adoption-notice-detail", args=[adoption_notice_instance.pk], ))
|
||||
else:
|
||||
form = AdoptionNoticeForm(instance=adoption_notice)
|
||||
@@ -164,14 +178,15 @@ def search(request):
|
||||
if max_distance == "":
|
||||
max_distance = None
|
||||
geo_api = GeoAPI()
|
||||
search_position = geo_api.get_coordinates_from_query(request.POST['postcode'])
|
||||
search_position = geo_api.get_coordinates_from_query(request.POST['location'])
|
||||
if search_position is None:
|
||||
place_not_found = True
|
||||
adoption_notices_in_distance = active_adoptions
|
||||
else:
|
||||
adoption_notices_in_distance = [a for a in active_adoptions if a.in_distance(search_position, max_distance)]
|
||||
|
||||
context = {"adoption_notices": adoption_notices_in_distance, "search_form": search_form, "place_not_found": place_not_found}
|
||||
|
||||
context = {"adoption_notices": adoption_notices_in_distance, "search_form": search_form,
|
||||
"place_not_found": place_not_found}
|
||||
else:
|
||||
latest_adoption_list = AdoptionNotice.objects.order_by("-created_at")
|
||||
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
|
||||
@@ -183,36 +198,36 @@ def search(request):
|
||||
@login_required
|
||||
def add_adoption_notice(request):
|
||||
if request.method == 'POST':
|
||||
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST, request.FILES, in_adoption_notice_creation_flow=True)
|
||||
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST, request.FILES,
|
||||
in_adoption_notice_creation_flow=True)
|
||||
|
||||
if form.is_valid():
|
||||
instance = form.save(commit=False)
|
||||
instance.owner = request.user
|
||||
"""Search the location given in the location string and add it to the adoption notice"""
|
||||
location = Location.get_location_from_string(instance.location_string)
|
||||
instance.location = location
|
||||
instance.save()
|
||||
|
||||
# Set correct status
|
||||
if request.user.trust_level >= User.TRUST_LEVEL[User.COORDINATOR]:
|
||||
major_status = AdoptionNoticeStatus.ACTIVE
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.ACTIVE]["searching"]
|
||||
else:
|
||||
major_status=AdoptionNoticeStatus.AWAITING_ACTION
|
||||
minor_status=AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION]["waiting_for_review"]
|
||||
status = AdoptionNoticeStatus.objects.create(major_status=major_status,
|
||||
minor_status=minor_status,
|
||||
adoption_notice=instance)
|
||||
status.save()
|
||||
"""Spin up a task that adds the location"""
|
||||
add_adoption_notice_location.delay_on_commit(instance.pk)
|
||||
|
||||
# Get the species and number of animals from the form
|
||||
# Set correct status
|
||||
if request.user.trust_level >= TrustLevel.MODERATOR:
|
||||
instance.set_active()
|
||||
else:
|
||||
instance.set_unchecked()
|
||||
|
||||
# Get the species and number of animals from the form
|
||||
species = form.cleaned_data["species"]
|
||||
sex = form.cleaned_data["sex"]
|
||||
num_animals = form.cleaned_data["num_animals"]
|
||||
date_of_birth = form.cleaned_data["date_of_birth"]
|
||||
for i in range(0, num_animals):
|
||||
Animal.objects.create(owner=request.user,
|
||||
name=f"{species} {i+1}", adoption_notice=instance, species=species, sex=sex, date_of_birth=date_of_birth)
|
||||
name=f"{species} {i + 1}", adoption_notice=instance, species=species, sex=sex,
|
||||
date_of_birth=date_of_birth)
|
||||
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="add_adoption_notice",
|
||||
text=f"{request.user} hat Vermittlung {instance.pk} hinzugefügt")
|
||||
return redirect(reverse("adoption-notice-detail", args=[instance.pk]))
|
||||
else:
|
||||
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True)
|
||||
@@ -257,6 +272,11 @@ def add_photo_to_animal(request, animal_id):
|
||||
instance.save()
|
||||
|
||||
animal.photos.add(instance)
|
||||
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
||||
text=f"{request.user} hat Foto {instance.pk} zum Tier {animal.pk} hinzugefügt")
|
||||
|
||||
if "save-and-add-another" in request.POST:
|
||||
form = ImageForm(in_flow=True)
|
||||
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
||||
@@ -280,6 +300,9 @@ def add_photo_to_adoption_notice(request, adoption_notice_id):
|
||||
instance.owner = request.user
|
||||
instance.save()
|
||||
adoption_notice.photos.add(instance)
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
||||
text=f"{request.user} hat Foto {instance.pk} zur Vermittlung {adoption_notice.pk} hinzugefügt")
|
||||
if "save-and-add-another" in request.POST:
|
||||
form = ImageForm(in_flow=True)
|
||||
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
||||
@@ -306,6 +329,10 @@ def animal_edit(request, animal_id):
|
||||
|
||||
if form.is_valid():
|
||||
animal = form.save()
|
||||
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
||||
text=f"{request.user} hat Tier {animal.pk} zum Tier geändert")
|
||||
return redirect(reverse("animal-detail", args=[animal.pk], ))
|
||||
else:
|
||||
form = AnimalForm(instance=animal)
|
||||
@@ -319,7 +346,7 @@ def about(request):
|
||||
lang = Language.objects.get(languagecode=language_code)
|
||||
|
||||
legal = {}
|
||||
for text_code in ["terms_of_service", "privacy_statement", "imprint"]:
|
||||
for text_code in ["terms_of_service", "privacy_statement", "imprint", "about_us"]:
|
||||
try:
|
||||
legal[text_code] = Text.objects.get(text_code=text_code, language=lang, )
|
||||
except Text.DoesNotExist:
|
||||
@@ -393,16 +420,41 @@ def report_detail_success(request, report_id):
|
||||
return report_detail(request, report_id, form_complete=True)
|
||||
|
||||
|
||||
def user_detail(request, user, token=None):
|
||||
context = {"user": user,
|
||||
"adoption_notices": AdoptionNotice.objects.filter(owner=user),
|
||||
"notifications": BaseNotification.objects.filter(user=user, read=False)}
|
||||
if token is not None:
|
||||
context["token"] = token
|
||||
return render(request, 'fellchensammlung/details/detail-user.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
def user_detail(request, user_id):
|
||||
def user_by_id(request, user_id):
|
||||
user = User.objects.get(id=user_id)
|
||||
# Only users that are mods or owners of the user are allowed to view
|
||||
fail_if_user_not_owner_or_trust_level(request.user, user)
|
||||
if request.method == "POST":
|
||||
if user == request.user:
|
||||
return my_profile(request)
|
||||
else:
|
||||
return user_detail(request, user)
|
||||
|
||||
|
||||
@login_required()
|
||||
def my_profile(request):
|
||||
if request.method == 'POST':
|
||||
if "create_token" in request.POST:
|
||||
Token.objects.create(user=request.user)
|
||||
elif "delete_token" in request.POST:
|
||||
Token.objects.get(user=request.user).delete()
|
||||
|
||||
action = request.POST.get("action")
|
||||
if action == "notification_mark_read":
|
||||
notification_id = request.POST.get("notification_id")
|
||||
notification = CommentNotification.objects.get(pk=notification_id)
|
||||
try:
|
||||
notification = CommentNotification.objects.get(pk=notification_id)
|
||||
except CommentNotification.DoesNotExist:
|
||||
notification = BaseNotification.objects.get(pk=notification_id)
|
||||
notification.read = True
|
||||
notification.save()
|
||||
elif action == "notification_mark_all_read":
|
||||
@@ -410,11 +462,11 @@ def user_detail(request, user_id):
|
||||
for notification in notifications:
|
||||
notification.read = True
|
||||
notification.save()
|
||||
|
||||
context = {"user": user,
|
||||
"adoption_notices": AdoptionNotice.objects.filter(owner=user),
|
||||
"notifications": CommentNotification.objects.filter(user=user, read=False)}
|
||||
return render(request, 'fellchensammlung/details/detail-user.html', context=context)
|
||||
try:
|
||||
token = Token.objects.get(user=request.user)
|
||||
except Token.DoesNotExist:
|
||||
token = None
|
||||
return user_detail(request, request.user, token)
|
||||
|
||||
|
||||
@user_passes_test(user_is_trust_level_or_above)
|
||||
@@ -423,31 +475,34 @@ def modqueue(request):
|
||||
context = {"reports": open_reports}
|
||||
return render(request, 'fellchensammlung/modqueue.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
def updatequeue(request):
|
||||
if request.method == "POST":
|
||||
print(request.POST.get("adoption_notice_id"))
|
||||
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
|
||||
edit_permission = request.user == adoption_notice.owner or user_is_trust_level_or_above(request.user,
|
||||
TrustLevel.MODERATOR)
|
||||
if not edit_permission:
|
||||
return render(request, "fellchensammlung/errors/403.html", status=403)
|
||||
action = request.POST.get("action")
|
||||
print(f"Action: {action}")
|
||||
if action == "checked_inactive":
|
||||
adoption_notice.set_closed()
|
||||
elif action == "checked_active":
|
||||
print("set checked")
|
||||
adoption_notice.set_checked()
|
||||
if action == "checked_active":
|
||||
adoption_notice.set_active()
|
||||
|
||||
if user_is_trust_level_or_above(request.user, User.MODERATOR):
|
||||
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
|
||||
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
|
||||
else:
|
||||
last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked")
|
||||
adoption_notices = [adoption for adoption in last_checked_adoption_list if adoption.is_active]
|
||||
|
||||
context = {"adoption_notices": adoption_notices}
|
||||
adoption_notices_active = [adoption for adoption in last_checked_adoption_list if adoption.is_active]
|
||||
adoption_notices_disabled = [adoption for adoption in last_checked_adoption_list if adoption.is_disabled_unchecked]
|
||||
context = {"adoption_notices_disabled": adoption_notices_disabled,
|
||||
"adoption_notices_active": adoption_notices_active}
|
||||
return render(request, 'fellchensammlung/updatequeue.html', context=context)
|
||||
|
||||
|
||||
def map(request):
|
||||
adoption_notices = AdoptionNotice.objects.all() #TODO: Filter to active
|
||||
adoption_notices = AdoptionNotice.objects.all() #TODO: Filter to active
|
||||
context = {"adoption_notices_map": adoption_notices}
|
||||
return render(request, 'fellchensammlung/map.html', context=context)
|
||||
|
||||
@@ -456,6 +511,7 @@ def metrics(request):
|
||||
data = gather_metrics_data()
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
@login_required
|
||||
def instance_health_check(request):
|
||||
"""
|
||||
@@ -465,16 +521,20 @@ def instance_health_check(request):
|
||||
action = request.POST.get("action")
|
||||
if action == "clean_locations":
|
||||
clean_locations(quiet=False)
|
||||
elif action == "deactivate_unchecked_adoption_notices":
|
||||
deactivate_unchecked_adoption_notices()
|
||||
|
||||
number_of_adoption_notices = AdoptionNotice.objects.all().count()
|
||||
none_geocoded_adoption_notices = AdoptionNotice.objects.filter(location__isnull=True)
|
||||
number_not_geocoded_adoption_notices = len(none_geocoded_adoption_notices)
|
||||
|
||||
|
||||
number_of_rescue_orgs = RescueOrganization.objects.all().count()
|
||||
none_geocoded_rescue_orgs = RescueOrganization.objects.filter(location__isnull=True)
|
||||
number_not_geocoded_rescue_orgs = len(none_geocoded_rescue_orgs)
|
||||
|
||||
unchecked_ans = get_unchecked_adoption_notices()
|
||||
number_unchecked_ans = len(unchecked_ans)
|
||||
|
||||
# CHECK FOR MISSING TEXTS
|
||||
languages = Language.objects.all()
|
||||
texts = Text.objects.all()
|
||||
@@ -487,6 +547,9 @@ def instance_health_check(request):
|
||||
except Text.DoesNotExist:
|
||||
missing_texts.append((text_code, language))
|
||||
|
||||
# Timestamps
|
||||
timestamps = Timestamp.objects.all()
|
||||
|
||||
context = {
|
||||
"number_of_adoption_notices": number_of_adoption_notices,
|
||||
"number_not_geocoded_adoption_notices": number_not_geocoded_adoption_notices,
|
||||
@@ -494,10 +557,38 @@ def instance_health_check(request):
|
||||
"number_of_rescue_orgs": number_of_rescue_orgs,
|
||||
"number_not_geocoded_rescue_orgs": number_not_geocoded_rescue_orgs,
|
||||
"none_geocoded_rescue_orgs": none_geocoded_rescue_orgs,
|
||||
"missing_texts": missing_texts
|
||||
"missing_texts": missing_texts,
|
||||
"number_unchecked_ans": number_unchecked_ans,
|
||||
"unchecked_ans": unchecked_ans,
|
||||
"timestamps": timestamps
|
||||
}
|
||||
|
||||
|
||||
return render(request, 'fellchensammlung/instance-health-check.html', context=context)
|
||||
|
||||
|
||||
def external_site_warning(request):
|
||||
url = request.GET.get("url")
|
||||
context = {"url": url}
|
||||
language_code = translation.get_language()
|
||||
lang = Language.objects.get(languagecode=language_code)
|
||||
texts = Text.get_texts(["external_site_warning", "good_adoption_practices"], language=lang)
|
||||
context.update(texts)
|
||||
|
||||
return render(request, 'fellchensammlung/external_site_warning.html', context=context)
|
||||
|
||||
|
||||
def detail_view_rescue_organization(request, rescue_organization_id):
|
||||
org = RescueOrganization.objects.get(pk=rescue_organization_id)
|
||||
return render(request, 'fellchensammlung/details/detail-rescue-organization.html', context={"org": org})
|
||||
|
||||
|
||||
def export_own_profile(request):
|
||||
user = request.user
|
||||
ANs = AdoptionNotice.objects.filter(owner=user)
|
||||
user_as_json = serialize('json', [user])
|
||||
user_editable = json.loads(user_as_json)
|
||||
user_editable[0]["fields"]["password"] = "Password hash redacted for security reasons"
|
||||
user_as_json = json.dumps(user_editable)
|
||||
ANs_as_json = serialize('json', ANs)
|
||||
full_json = f"{user_as_json}, {ANs_as_json}"
|
||||
return HttpResponse(full_json, content_type="application/json")
|
||||
|
@@ -1 +1,8 @@
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.3.1"
|
||||
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
|
||||
|
33
src/notfellchen/celery.py
Normal file
33
src/notfellchen/celery.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
from notfellchen import settings
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'notfellchen.settings')
|
||||
|
||||
app = Celery('notfellchen')
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
app.autodiscover_tasks()
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
'daily-cleanup': {
|
||||
'task': 'admin.clean_locations',
|
||||
'schedule': crontab(hour=2),
|
||||
},
|
||||
'daily-unchecked-deactivation': {
|
||||
'task': 'admin.daily_unchecked_deactivation',
|
||||
'schedule': crontab(hour=1),
|
||||
},
|
||||
'daily-404-deactivation': {
|
||||
'task': 'admin.deactivate_404_adoption_notices',
|
||||
'schedule': crontab(hour=3),
|
||||
},
|
||||
}
|
||||
|
||||
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":
|
||||
# If a healthcheck is configured, this will send an hourly ping to the healthchecks server
|
||||
app.conf.beat_schedule['hourly-healthcheck'] = {'task': 'tools.healthcheck',
|
||||
'schedule': crontab(minute=43),
|
||||
}
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
||||
import os
|
||||
import configparser
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from celery import Celery
|
||||
|
||||
"""CONFIG PARSER """
|
||||
config = configparser.RawConfigParser()
|
||||
@@ -75,16 +76,23 @@ DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3")
|
||||
DB_USER = config.get("database", "user", fallback='')
|
||||
DB_PASSWORD = config.get("database", "password", fallback='')
|
||||
DB_HOST = config.get("database", "host", fallback='')
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
|
||||
|
||||
""" CELERY + KEYDB """
|
||||
CELERY_BROKER_URL = config.get("celery", "broker", fallback="redis://localhost:6379/0")
|
||||
CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localhost:6379/0")
|
||||
|
||||
""" MONITORING """
|
||||
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
|
||||
|
||||
""" GEOCODING """
|
||||
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
|
||||
""" Tile Server """
|
||||
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
|
||||
|
||||
|
||||
""" OxiTraffic"""
|
||||
OXITRAFFIC_ENABLED = config.get("tracking", "oxitraffic_enabled", fallback=False)
|
||||
OXITRAFFIC_BASE_URL = config.get("tracking", "oxitraffic_base_url", fallback="")
|
||||
@@ -161,6 +169,7 @@ INSTALLED_APPS = [
|
||||
'crispy_forms',
|
||||
"crispy_bootstrap4",
|
||||
"rest_framework",
|
||||
'rest_framework.authtoken'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -179,6 +188,8 @@ MIDDLEWARE = [
|
||||
|
||||
ROOT_URLCONF = 'notfellchen.urls'
|
||||
|
||||
SETTINGS_PATH = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
|
@@ -25,9 +25,9 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns (
|
||||
urlpatterns += i18n_patterns(
|
||||
path("", include("fellchensammlung.urls")),
|
||||
prefix_default_language = False
|
||||
prefix_default_language=False
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
@@ -1,8 +1,10 @@
|
||||
{% load i18n %}
|
||||
{% trans "Account aktivieren" %} {{ site.name }}:
|
||||
{{ site.name }}: {% trans "Account aktivieren" %}
|
||||
|
||||
<a href="{{ site.domain }}{% url 'django_registration_activate' activation_key%}">{% trans "Activate by clicking this link" %}</a>
|
||||
{% trans "oder öffne den folgenden link im Browser" %}:
|
||||
{{ site.domain }}{% url 'django_registration_activate' activation_key%}
|
||||
{% trans 'Hier ist dein Aktivierungs-Key. Mit diesem kannst du deinen Account freischalten.' %}
|
||||
{{ activation_key }}
|
||||
|
||||
{% blocktrans %}Der link ist gültig für {{ expiration_days }} tage.{% endblocktrans %}
|
||||
{% trans "Öffne den folgenden link im Browser und gib den Aktivierungs-Key dort ein" %}:
|
||||
https://{{ site.domain }}{% url 'django_registration_activate' %}
|
||||
|
||||
{% blocktrans %}Der Link ist für {{ expiration_days }} Tage gültig.{% endblocktrans %}
|
@@ -1 +1 @@
|
||||
{% load i18n %}{% translate "Account aktivieren" %} {{ site.name }}
|
||||
{% load i18n %}{{ site.name }}: {% translate "Account aktivieren" %}
|
15
src/templates/django_registration/activation_form.html
Normal file
15
src/templates/django_registration/activation_form.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
{% if not user.is_authenticated %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" class="btn2" value={% translate 'Absenden' %}>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% translate "Du bist bereits eingeloggt." %}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -2,5 +2,5 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<p>{% translate "Du bist nun registriert. Du hast eine E-Mail mit einem Link zur aktivierung bekommen." %}</p>
|
||||
<p>{% translate "Du bist nun registriert. Du hast eine E-Mail mit einem Link zur Aktivierung deines Kontos bekommen." %}</p>
|
||||
{% endblock %}
|
97
src/tests/test_admin_tasks.py
Normal file
97
src/tests/test_admin_tasks.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||
deactivate_404_adoption_notices
|
||||
from fellchensammlung.tools.misc import is_404
|
||||
from django.test import TestCase
|
||||
|
||||
from model_bakery import baker
|
||||
from fellchensammlung.models import AdoptionNotice
|
||||
|
||||
|
||||
class DeactiviationTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
now = timezone.now()
|
||||
more_than_three_weeks_ago = now - timedelta(weeks=3, days=2)
|
||||
less_than_three_weeks_ago = now - timedelta(weeks=1, days=2)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption1",
|
||||
created_at=more_than_three_weeks_ago,
|
||||
last_checked=more_than_three_weeks_ago)
|
||||
cls.adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
cls.adoption3 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption3",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago)
|
||||
|
||||
cls.adoption1.set_active()
|
||||
cls.adoption3.set_active()
|
||||
|
||||
def test_get_unchecked_adoption_notices(self):
|
||||
result = get_unchecked_adoption_notices()
|
||||
|
||||
self.assertIn(self.adoption1, result)
|
||||
self.assertNotIn(self.adoption2, result)
|
||||
self.assertNotIn(self.adoption3, result)
|
||||
|
||||
def test_deactivate_unchecked_adoption_notices(self):
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
self.assertFalse(self.adoption2.is_active)
|
||||
self.assertTrue(self.adoption3.is_active)
|
||||
|
||||
deactivate_unchecked_adoption_notices()
|
||||
|
||||
self.adoption1.refresh_from_db()
|
||||
self.adoption2.refresh_from_db()
|
||||
self.adoption3.refresh_from_db()
|
||||
|
||||
self.assertFalse(self.adoption1.is_active)
|
||||
self.assertFalse(self.adoption2.is_active)
|
||||
self.assertTrue(self.adoption3.is_active)
|
||||
|
||||
|
||||
class PingTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
link_active = "https://hyteck.de/"
|
||||
link_inactive = "https://hyteck.de/maxwell"
|
||||
now = timezone.now()
|
||||
less_than_three_weeks_ago = now - timedelta(weeks=1, days=2)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption1",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago,
|
||||
further_information=link_active)
|
||||
cls.adoption2 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption2",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago,
|
||||
further_information=link_inactive)
|
||||
cls.adoption3 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption3",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago,
|
||||
further_information=None)
|
||||
cls.adoption1.set_active()
|
||||
cls.adoption2.set_active()
|
||||
cls.adoption3.set_active()
|
||||
|
||||
def test_is_404(self):
|
||||
urls = [("https://hyteck.de/maxwell", True),
|
||||
("https://hyteck.de", False)]
|
||||
for url, expected_result in urls:
|
||||
self.assertEqual(is_404(url), expected_result)
|
||||
|
||||
def test_deactivate_404_adoption_notices(self):
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
self.assertTrue(self.adoption2.is_active)
|
||||
deactivate_404_adoption_notices()
|
||||
self.adoption1.refresh_from_db()
|
||||
self.adoption2.refresh_from_db()
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
self.assertFalse(self.adoption2.is_active)
|
||||
|
25
src/tests/test_forms.py
Normal file
25
src/tests/test_forms.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.test import TestCase
|
||||
from fellchensammlung.forms import AdoptionNoticeFormWithDateWidgetAutoAnimal
|
||||
from fellchensammlung.models import Species
|
||||
from model_bakery import baker
|
||||
|
||||
|
||||
class TestAdoptionNoticeFormWithDateWidgetAutoAnimal(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
rat = baker.make(Species, name="Farbratte")
|
||||
|
||||
def test_forms(self):
|
||||
form_data = {"name": "TestAdoption3",
|
||||
"species": Species.objects.first(),
|
||||
"num_animals": "2",
|
||||
"date_of_birth": "2024-11-04",
|
||||
"sex": "M",
|
||||
"group_only": "on",
|
||||
"searching_since": "2024-11-10",
|
||||
"location_string": "Mannheim",
|
||||
"description": "Blaaaa",
|
||||
"further_information": "https://notfellchen.org",
|
||||
"save-and-add-another-animal": "Speichern"}
|
||||
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(data=form_data)
|
||||
self.assertTrue(form.is_valid())
|
24
src/tests/test_geo.py
Normal file
24
src/tests/test_geo.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fellchensammlung.tools.geo import calculate_distance_between_coordinates
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class DistanceTest(TestCase):
|
||||
accuracy = 1.05 # 5% off is ok
|
||||
|
||||
def test_calculate_distance_between_coordinates(self):
|
||||
coordinates_berlin = (52.50327,13.41238)
|
||||
coordinates_stuttgart = (48.77753579028781, 9.185250111016634)
|
||||
coordinates_weil_im_dorf = (48.813691653929276, 9.112217733791029)
|
||||
coordinates_with_distance = {"berlin_stuttgart": (coordinates_berlin, coordinates_stuttgart, 510),
|
||||
"stuttgart_berlin": (coordinates_stuttgart, coordinates_berlin, 510),
|
||||
"stuttgart_weil": (coordinates_stuttgart, coordinates_weil_im_dorf, 6.7),
|
||||
}
|
||||
for key in coordinates_with_distance:
|
||||
(a, b, distance) = coordinates_with_distance[key]
|
||||
result = calculate_distance_between_coordinates(a, b)
|
||||
try:
|
||||
self.assertLess(result, distance * self.accuracy)
|
||||
self.assertGreater(result, distance / self.accuracy)
|
||||
except AssertionError as e:
|
||||
print(f"Distance calculation failed. Expected {distance}, got {result}")
|
||||
raise e
|
@@ -4,7 +4,15 @@ from django.utils import timezone
|
||||
from django.test import TestCase
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import Announcement, Language
|
||||
from fellchensammlung.models import Announcement, Language, User, TrustLevel
|
||||
|
||||
|
||||
class UserTest(TestCase):
|
||||
def test_creating_user(self):
|
||||
test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
|
||||
|
||||
self.assertTrue(test_user_1.trust_level == 1)
|
||||
self.assertTrue(test_user_1.trust_level == TrustLevel.MEMBER)
|
||||
|
||||
|
||||
class AnnouncementTest(TestCase):
|
||||
@@ -69,4 +77,3 @@ class AnnouncementTest(TestCase):
|
||||
self.assertTrue(self.announcement2 not in active_announcements)
|
||||
self.assertTrue(self.announcement4 not in active_announcements)
|
||||
self.assertTrue(self.announcement5 in active_announcements)
|
||||
|
||||
|
@@ -4,7 +4,9 @@ from django.urls import reverse
|
||||
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User
|
||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel
|
||||
from fellchensammlung.views import add_adoption_notice
|
||||
|
||||
|
||||
class AnimalAndAdoptionTest(TestCase):
|
||||
@classmethod
|
||||
@@ -18,7 +20,8 @@ class AnimalAndAdoptionTest(TestCase):
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
test_user1.save()
|
||||
test_user0.trust_level = TrustLevel.ADMIN
|
||||
test_user0.save()
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
rat = baker.make(Species, name="Farbratte")
|
||||
@@ -47,3 +50,146 @@ class AnimalAndAdoptionTest(TestCase):
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "Rat1")
|
||||
|
||||
def test_creating_AN_as_admin(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
form_data = {"name": "TestAdoption4",
|
||||
"species": Species.objects.first().pk,
|
||||
"num_animals": "2",
|
||||
"date_of_birth": "2024-11-04",
|
||||
"sex": "M",
|
||||
"group_only": "on",
|
||||
"searching_since": "2024-11-10",
|
||||
"location_string": "Mannheim",
|
||||
"description": "Blaaaa",
|
||||
"further_information": "https://notfellchen.org",
|
||||
"save-and-add-another-animal": "Speichern"}
|
||||
|
||||
response = self.client.post(reverse('add-adoption'), data=form_data)
|
||||
print(response.content)
|
||||
|
||||
self.assertTrue(response.status_code < 400)
|
||||
self.assertTrue(AdoptionNotice.objects.get(name="TestAdoption4").is_active)
|
||||
|
||||
|
||||
|
||||
class SearchTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
berlin = Location.get_location_from_string("Berlin")
|
||||
adoption1.location = berlin
|
||||
adoption1.save()
|
||||
stuttgart = Location.get_location_from_string("Tübingen")
|
||||
adoption3.location = stuttgart
|
||||
adoption3.save()
|
||||
|
||||
adoption1.set_active()
|
||||
adoption3.set_active()
|
||||
adoption2.set_unchecked()
|
||||
|
||||
def test_basic_view(self):
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
|
||||
def test_basic_view_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
def test_plz_search(self):
|
||||
response = self.client.post(reverse('search'), {"max_distance": 100, "location": "Berlin"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption3")
|
||||
|
||||
|
||||
class UpdateQueueTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345',
|
||||
trust_level=TrustLevel.MODERATOR)
|
||||
test_user0.is_superuser = True
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
cls.adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
cls.adoption1.set_unchecked()
|
||||
cls.adoption3.set_unchecked()
|
||||
|
||||
def test_login_required(self):
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEquals(response.url, "/accounts/login/?next=/updatequeue/")
|
||||
|
||||
def test_set_updated(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Make sure Adoption1 is in response
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption1.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'), {"adoption_notice_id": self.adoption1.pk,
|
||||
"action": "checked_active"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption1.refresh_from_db()
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
|
||||
def test_set_checked_inactive(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure Adoption3 is in response
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'),
|
||||
{"adoption_notice_id": self.adoption3.id, "action": "checked_inactive"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption3.refresh_from_db()
|
||||
|
||||
# Make sure correct status is set and AN is not shown anymore
|
||||
self.assertNotContains(response, "TestAdoption3")
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
self.assertEqual(self.adoption3.adoptionnoticestatus.major_status, AdoptionNoticeStatus.CLOSED)
|
||||
|
||||
|
Reference in New Issue
Block a user