Compare commits
No commits in common. "main" and "ci" have entirely different histories.
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,15 +0,0 @@
|
|||||||
## Version 0.4.0
|
|
||||||
|
|
||||||
Version 0.4.0 has added support for search-as-you-type when searching for animals to adopt. Furthermore, the display of
|
|
||||||
maps in the search has been majorly improved.
|
|
||||||
|
|
||||||
Photon has been added as geocoding source option which allows to use this functionality.
|
|
||||||
|
|
||||||
Further improvements include the representation of rescue organizations and tooltips.
|
|
||||||
|
|
||||||
One of the biggest features is the addition of search subscriptions. These allow you to not only
|
|
||||||
search for currently active adoption notices but to subscribe to that search so that you get notified if there are new
|
|
||||||
rats in your search area in the future.
|
|
||||||
|
|
||||||
For developers the new API documentation might come in handy, it can be found at
|
|
||||||
[/api/schema/swagger-ui/](https://notfellchen.org/api/schema/swagger-ui/)
|
|
@ -1,12 +1,10 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3-slim
|
||||||
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
|
|
||||||
MAINTAINER Julian-Samuel Gebühr
|
MAINTAINER Julian-Samuel Gebühr
|
||||||
|
|
||||||
ENV DOCKER_BUILD=true
|
ENV DOCKER_BUILD=true
|
||||||
|
|
||||||
RUN apt update
|
RUN apt update
|
||||||
RUN apt install gettext -y
|
RUN apt install gettext -y
|
||||||
RUN apt install libpq-dev gcc -y
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN mkdir /app/data
|
RUN mkdir /app/data
|
||||||
|
62
README.md
62
README.md
@ -44,16 +44,14 @@ 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.
|
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.
|
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 |
|
| Textcode | Location |
|
||||||
|-------------------------|-----------------------|
|
|---------------------|----------|
|
||||||
| `how_to` | Index |
|
| `how_to` | Index |
|
||||||
| `introduction` | Index |
|
| `introduction` | Index |
|
||||||
| `privacy_statement` | About |
|
| `privacy_statement` | About |
|
||||||
| `terms_of_service` | About |
|
| `terms_of_service` | About |
|
||||||
| `imprint` | About |
|
| `imprint` | About |
|
||||||
| `about_us` | About |
|
| Any rule | About |
|
||||||
| `external_site_warning` | External Site Warning |
|
|
||||||
| Any rule | About |
|
|
||||||
|
|
||||||
# Developer Notes
|
# Developer Notes
|
||||||
|
|
||||||
@ -80,13 +78,17 @@ docker run -p8000:7345 moanos/notfellchen:latest
|
|||||||
## Geocoding
|
## Geocoding
|
||||||
|
|
||||||
Geocoding services (search map data by name, address or postcode) are provided via the
|
Geocoding services (search map data by name, address or postcode) are provided via the
|
||||||
either [Nominatim](https://nominatim.org/) or [photon](https://github.com/komoot/photon) API, powered by [OpenStreetMap](https://openstreetmap.org) data.
|
[Nominatim](https://nominatim.org/) API, powered by [OpenStreetMap](https://openstreetmap.org) data. Notfellchen uses
|
||||||
Notfellchen uses a selfhosted Photon instance to avoid overburdening the publicly hosted instance.
|
a selfhosted Nominatim instance to avoid overburdening the publicly hosted instance. Due to ressource constraints
|
||||||
|
geocoding is only supported for Germany right now.
|
||||||
|
|
||||||
|
ToDos
|
||||||
|
* [ ] Implement a report that shows the number of location strings that could not be converted into a location
|
||||||
|
* [x] Add a management command to re-query location strings to fill location
|
||||||
|
|
||||||
## Maps
|
## Maps
|
||||||
|
|
||||||
The map on the main homepage is powered by [Versatiles](https://versatiles.org), and rendered using [Maplibre](https://maplibre.org/).
|
The map on the main homepage is powered by [Versatiles](https://versatiles.org), and rendered using [Maplibre](https://maplibre.org/).
|
||||||
The Versatiles server is self-hosted and does not send data to third parties.
|
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
@ -104,37 +106,3 @@ Use a program like `gtranslator` or `poedit` to start translations
|
|||||||
| Edit adoption notice | User that created, Moderator, Admin |
|
| Edit adoption notice | User that created, Moderator, Admin |
|
||||||
| Edit animal | User that created, Moderator, Admin |
|
| Edit animal | User that created, Moderator, Admin |
|
||||||
| Add animal/photo to adoption notice | 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
|
|
||||||
```
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
This project is currently solely developed by me, moanos. I'd like that to change and will be very happy for contributions
|
|
||||||
and shared responsibilities. Some ideas where you can look for contributing first
|
|
||||||
|
|
||||||
* CSS structure: It's a hot mess right now, and I'm happy it somehow works. As you might see, there is much room for improvement. Refactoring this and streamlining the look across the app would be amazing.
|
|
||||||
* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
|
|
||||||
* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
|
|
||||||
|
|
||||||
I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
|
|
||||||
issue for it before you spend a lot of work.
|
|
||||||
|
|
||||||
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [Github](https://github.com/moan0s/notfellchen).
|
|
||||||
CI (currently only for dcumentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
|
|
||||||
|
|
||||||
Also welcome are new issues with suggestions or bugs and additions to the documentation.
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
API Documentation
|
API Documentation
|
||||||
*****************
|
*****************
|
||||||
|
|
||||||
The Notfellchen API serves the purpose of supporting 3rd-person applications, whether you want to display data in a custom format or add data from other sources.
|
The Notfellchen API serves the purpose of supporting 3rd-person applications and anything you can think of basically.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
The current API is limited in it's functionality. I you miss a specific feature please contact the developers!
|
The current API is limited in it's functionality. I you miss a specific feature please contact the developer!
|
||||||
|
|
||||||
API Access
|
API Access
|
||||||
==========
|
==========
|
||||||
@ -14,83 +14,17 @@ Via browser
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
|
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
|
||||||
The API endpoint can be found at http://notfellchen.org/api/adoption_notices
|
The API endpoint can be found at /library/api/
|
||||||
|
http://notfellchen.org/
|
||||||
|
|
||||||
Via token
|
Via token
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This is currently not supported.
|
||||||
|
|
||||||
All users are able to generate a token that allows them to use the API. This can be done in the user's profile.
|
All users are able to generate a token that allows them to use the API. This can be done in the user's profile.
|
||||||
An application can then send this token in the request header for authorization.
|
An application can then send this token in the request header for authorization.
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
$ curl -X GET http://notfellchen.org/api/adoption_notice -H 'Authorization: Token 49b39856955dc6e5cc04365498d4ad30ea3aed78'
|
$ curl -X GET http://notfellchen.org/api/adoption_notice -H 'Authorization: Token 49b39856955dc6e5cc04365498d4ad30ea3aed78'
|
||||||
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Usage or creation of content still has to follow the terms of Notfellchen.org
|
|
||||||
Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
|
|
||||||
Talk to the Notfellchen-Team if you want develop such things.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
Get Adoption Notices
|
|
||||||
++++++++++++++++++++
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
curl --request GET \
|
|
||||||
--url https://notfellchen.org/api/adoption_notice \
|
|
||||||
--header 'Authorization: {{token}}'
|
|
||||||
|
|
||||||
Create Adoption Notice
|
|
||||||
++++++++++++++++++++++
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
curl --request POST \
|
|
||||||
--url https://notfellchen.org/api/adoption_notice \
|
|
||||||
--header 'Authorization: {{token}}' \
|
|
||||||
--header 'content-type: multipart/form-data' \
|
|
||||||
--form name=TestAdoption1 \
|
|
||||||
--form searching_since=2024-11-19 \
|
|
||||||
--form 'description=Lorem ipsum **dolor sit** amet' \
|
|
||||||
--form further_information=https://notfellchen.org \
|
|
||||||
--form location_string=Berlin \
|
|
||||||
--form group_only=true
|
|
||||||
|
|
||||||
Add Animal to Adoption Notice
|
|
||||||
+++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
curl --request POST \
|
|
||||||
--url https://notfellchen.org/api/animals/ \
|
|
||||||
--header 'Authorization: {{token}}' \
|
|
||||||
--header 'content-type: multipart/form-data' \
|
|
||||||
--form name=TestAnimal1 \
|
|
||||||
--form date_of_birth=2024-11-19 \
|
|
||||||
--form 'description=Lorem animal **dolor sit**.' \
|
|
||||||
--form sex=F \
|
|
||||||
--form species=1 \
|
|
||||||
--form adoption_notice=1
|
|
||||||
|
|
||||||
Add picture to Animal or Adoption Notice
|
|
||||||
++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
curl -X POST https://notfellchen.org/api/images/ \
|
|
||||||
-H "Authorization: Token {{token}}" \
|
|
||||||
-F "image=@256-256-crop.jpg" \
|
|
||||||
-F "alt_text=Puppy enjoying the sunshine" \
|
|
||||||
-F "attach_to_type=animal" \
|
|
||||||
-F "attach_to=48
|
|
||||||
|
|
||||||
Species
|
|
||||||
+++++++
|
|
||||||
|
|
||||||
Getting available species is mainly important when creating animals
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
curl --request GET \
|
|
||||||
--url https://notfellchen.org/api/species \
|
|
||||||
--header 'Authorization: {{token}}'
|
|
||||||
|
@ -4,5 +4,9 @@ Administration
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
GDPR.rst
|
create_user.rst
|
||||||
|
lending.rst
|
||||||
|
returning.rst
|
||||||
|
opening_hours.rst
|
||||||
|
add_items.rst
|
||||||
monitoring.rst
|
monitoring.rst
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
Monitoring
|
Monitoring
|
||||||
==========
|
==========
|
||||||
|
|
||||||
Notfellchen should, like every other software, be easy to monitor. Therefore a basic metrics are exposed to `https://notfellchen.org/metrics`.
|
ILMO 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.
|
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,12 +60,3 @@ Now we can simply use the InfluxDB as data source in Grafana and configure until
|
|||||||
beautiful plots!
|
beautiful plots!
|
||||||
|
|
||||||
.. image:: monitoring_grafana.png
|
.. 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
|
|
||||||
|
10
docs/admin/opening_hours.rst
Normal file
10
docs/admin/opening_hours.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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 "-".
|
8
docs/admin/returning.rst
Normal file
8
docs/admin/returning.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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 information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'Notfellchen'
|
project = 'Notfellchen'
|
||||||
copyright = 'CC-BY-SA Julian-Samuel Gebühr'
|
copyright = 'Julian-Samuel Gebühr'
|
||||||
author = 'Julian-Samuel Gebühr'
|
author = 'Julian-Samuel Gebühr'
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
|
@ -4,16 +4,16 @@
|
|||||||
Deployment
|
Deployment
|
||||||
**********
|
**********
|
||||||
|
|
||||||
There are different ways to deploy Notfellchen. We support an ansible+docker based deployment and manual installation.
|
There are different ways to deploy ILMO. We support an ansible+docker based deployment and manual installation.
|
||||||
|
|
||||||
Ansible deployment
|
Ansible deployment
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Notfellchen can be deployed with the `notfellchen-ansible-role <https://github.com/moan0s/ansible-role-notfellchen>`_ that is based on the
|
ILMO can be deployed with the `ilmo-ansible-role <https://github.com/moan0s/ansible-role-ilmo>`_ 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
|
official ILMO docker image. This role will only install ilmo itself. If you want a complete setup that includes a
|
||||||
database and a webserver with minimal configuration you can use the
|
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
|
`mash-playbook <https://github.com/mother-of-all-self-hosting/mash-playbook>`_ by following `it's documentation
|
||||||
on Notfellchen <https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/notfellchen.md>`_.
|
on ILMO <https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/ilmo.md>`_.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -21,10 +21,10 @@ Manual Deployment
|
|||||||
=================
|
=================
|
||||||
|
|
||||||
|
|
||||||
This guide describes the installation of a installation of Notfellchen from source. It is inspired by this great guide from
|
This guide describes the installation of a installation of ILMO from source. It is inspired by this great guide from
|
||||||
pretix_.
|
pretix_.
|
||||||
|
|
||||||
.. warning:: Even though this guide tries to make it as straightforward to run Notfellchen, it still requires some Linux experience to
|
.. warning:: Even though this guide tries to make it as straightforward to run ILMO, 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_.
|
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.
|
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 HTTP reverse proxy, e.g. `nginx`_ or Traefik to allow HTTPS connections
|
||||||
* A `PostgreSQL`_ database server
|
* A `PostgreSQL`_ database server
|
||||||
|
|
||||||
Also recommended is, that you use a firewall, although this is not a Notfellchen-specific recommendation. If you're new to
|
Also recommended is, that you use a firewall, although this is not a ILMO-specific recommendation. If you're new to
|
||||||
Linux and firewalls, it is recommended that you start with `ufw`_.
|
Linux and firewalls, it is recommended that you start with `ufw`_.
|
||||||
|
|
||||||
.. note:: Please, do not run Notfellchen without HTTPS encryption. You'll handle user data and thanks to `Let's Encrypt`_
|
.. note:: Please, do not run ILMO without HTTPS encryption. You'll handle user data and thanks to `Let's Encrypt`_
|
||||||
SSL certificates can be obtained for free these days.
|
SSL certificates can be obtained for free these days.
|
||||||
|
|
||||||
Unix user
|
Unix user
|
||||||
---------
|
---------
|
||||||
|
|
||||||
As we do not want to run notfellchen as root, we first create a new unprivileged user::
|
As we do not want to run ilmo as root, we first create a new unprivileged user::
|
||||||
|
|
||||||
# adduser notfellchen --disabled-password --home /var/notfellchen
|
# adduser ilmo --disabled-password --home /var/ilmo
|
||||||
|
|
||||||
In this guide, all code lines prepended with a ``#`` symbol are commands that you need to execute on your server as
|
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.
|
``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::
|
For PostgreSQL database creation, we would do::
|
||||||
|
|
||||||
# sudo -u postgres createuser notfellchen
|
# sudo -u postgres createuser ilmo
|
||||||
# sudo -u postgres createdb -O notfellchen notfellchen
|
# sudo -u postgres createdb -O ilmo ilmo
|
||||||
# su notfellchen
|
# su ilmo
|
||||||
$ psql
|
$ psql
|
||||||
> ALTER USER notfellchen PASSWORD 'strong_password';
|
> ALTER USER ilmo PASSWORD 'strong_password';
|
||||||
|
|
||||||
Package dependencies
|
Package dependencies
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
To build and run notfellchen, you will need the following debian packages::
|
To build and run ilmo, you will need the following debian packages::
|
||||||
|
|
||||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
||||||
python3-dev
|
python3-dev
|
||||||
@ -83,32 +83,32 @@ To build and run notfellchen, you will need the following debian packages::
|
|||||||
Config file
|
Config file
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
We now create a config directory and config file for notfellchen::
|
We now create a config directory and config file for ilmo::
|
||||||
|
|
||||||
# mkdir /etc/notfellchen
|
# mkdir /etc/ilmo
|
||||||
# touch /etc/notfellchen/notfellchen.cfg
|
# touch /etc/ilmo/ilmo.cfg
|
||||||
# chown -R notfellchen:notfellchen /etc/notfellchen/
|
# chown -R ilmo:ilmo /etc/ilmo/
|
||||||
# chmod 0600 /etc/notfellchen/notfellchen.cfg
|
# chmod 0600 /etc/ilmo/ilmo.cfg
|
||||||
|
|
||||||
Fill the configuration file ``/etc/notfellchen/notfellchen.cfg`` with the following content (adjusted to your environment)::
|
Fill the configuration file ``/etc/ilmo/ilmo.cfg`` with the following content (adjusted to your environment)::
|
||||||
|
|
||||||
[notfellchen]
|
[ilmo]
|
||||||
instance_name=My library
|
instance_name=My library
|
||||||
url=https://notfellchen.example.com
|
url=https://ilmo.example.com
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
backend=postgresql
|
backend=postgresql
|
||||||
name=notfellchen
|
name=ilmo
|
||||||
user=notfellchen
|
user=ilmo
|
||||||
|
|
||||||
[locations]
|
[locations]
|
||||||
static=/var/notfellchen/static
|
static=/var/ilmo/static
|
||||||
|
|
||||||
[mail]
|
[mail]
|
||||||
; See config file documentation for more options
|
; See config file documentation for more options
|
||||||
; from=notfellchen@example.com
|
; from=ilmo@example.com
|
||||||
; host=127.0.0.1
|
; host=127.0.0.1
|
||||||
; user=notfellchen
|
; user=ilmo
|
||||||
; password=foobar
|
; password=foobar
|
||||||
; port=587
|
; port=587
|
||||||
|
|
||||||
@ -121,21 +121,21 @@ Fill the configuration file ``/etc/notfellchen/notfellchen.cfg`` with the follow
|
|||||||
;Scope=
|
;Scope=
|
||||||
;Policy=
|
;Policy=
|
||||||
|
|
||||||
Install notfellchen as package
|
Install ilmo as package
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
Now we will install notfellchen itself. The following steps are to be executed as the ``notfellchen`` user. Before we
|
Now we will install ilmo itself. The following steps are to be executed as the ``ilmo`` user. Before we
|
||||||
actually install notfellchen, we will create a virtual environment to isolate the python packages from your global
|
actually install ilmo, we will create a virtual environment to isolate the python packages from your global
|
||||||
python installation::
|
python installation::
|
||||||
|
|
||||||
$ python3 -m venv /var/notfellchen/venv
|
$ python3 -m venv /var/ilmo/venv
|
||||||
$ source /var/notfellchen/venv/bin/activate
|
$ source /var/ilmo/venv/bin/activate
|
||||||
(venv)$ pip3 install -U pip setuptools wheel
|
(venv)$ pip3 install -U pip setuptools wheel
|
||||||
|
|
||||||
We now clone and install notfellchen, its direct dependencies and gunicorn::
|
We now clone and install ilmo, its direct dependencies and gunicorn::
|
||||||
|
|
||||||
(venv)$ git clone https://github.com/moan0s/Notfellchen2
|
(venv)$ git clone https://github.com/moan0s/ILMO2
|
||||||
(venv)$ cd Notfellchen2/src/
|
(venv)$ cd ILMO2/src/
|
||||||
(venv)$ pip3 install -r requirements.txt
|
(venv)$ pip3 install -r requirements.txt
|
||||||
(venv)$ pip3 install -e .
|
(venv)$ pip3 install -e .
|
||||||
|
|
||||||
@ -148,26 +148,26 @@ Finally, we compile static files and create the database structure::
|
|||||||
(venv)$ django-admin compilemessages --ignore venv
|
(venv)$ django-admin compilemessages --ignore venv
|
||||||
|
|
||||||
|
|
||||||
Start notfellchen as a service
|
Start ilmo as a service
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
You should start notfellchen using systemd to automatically start it after a reboot. Create a file
|
You should start ilmo using systemd to automatically start it after a reboot. Create a file
|
||||||
named ``/etc/systemd/system/notfellchen-web.service`` with the following content::
|
named ``/etc/systemd/system/ilmo-web.service`` with the following content::
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=notfellchen web service
|
Description=ilmo web service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=notfellchen
|
User=ilmo
|
||||||
Group=notfellchen
|
Group=ilmo
|
||||||
Environment="VIRTUAL_ENV=/var/notfellchen/venv"
|
Environment="VIRTUAL_ENV=/var/ilmo/venv"
|
||||||
Environment="PATH=/var/notfellchen/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
Environment="PATH=/var/ilmo/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||||
ExecStart=/var/notfellchen/venv/bin/gunicorn notfellchen.wsgi \
|
ExecStart=/var/ilmo/venv/bin/gunicorn ilmo.wsgi \
|
||||||
--name notfellchen --workers 5 \
|
--name ilmo --workers 5 \
|
||||||
--max-requests 1200 --max-requests-jitter 50 \
|
--max-requests 1200 --max-requests-jitter 50 \
|
||||||
--log-level=info --bind=127.0.0.1:8345
|
--log-level=info --bind=127.0.0.1:8345
|
||||||
WorkingDirectory=/var/notfellchen
|
WorkingDirectory=/var/ilmo
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
@ -176,14 +176,14 @@ named ``/etc/systemd/system/notfellchen-web.service`` with the following content
|
|||||||
You can now run the following commands to enable and start the services::
|
You can now run the following commands to enable and start the services::
|
||||||
|
|
||||||
# systemctl daemon-reload
|
# systemctl daemon-reload
|
||||||
# systemctl enable notfellchen-web
|
# systemctl enable ilmo-web
|
||||||
# systemctl start notfellchen-web
|
# systemctl start ilmo-web
|
||||||
|
|
||||||
|
|
||||||
SSL
|
SSL
|
||||||
---
|
---
|
||||||
|
|
||||||
The following snippet is an example on how to configure a nginx proxy for notfellchen::
|
The following snippet is an example on how to configure a nginx proxy for ilmo::
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@ -196,8 +196,8 @@ The following snippet is an example on how to configure a nginx proxy for notfel
|
|||||||
#
|
#
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
ssl_certificate /etc/letsencrypt/live/notfellchen.example.com/cert.pem;
|
ssl_certificate /etc/letsencrypt/live/ilmo.example.com/cert.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/notfellchen.example.com/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/ilmo.example.com/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ The following snippet is an example on how to configure a nginx proxy for notfel
|
|||||||
add_header Referrer-Policy same-origin;
|
add_header Referrer-Policy same-origin;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
|
||||||
server_name notfellchen.example.com;
|
server_name ilmo.example.com;
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:8345;
|
proxy_pass http://localhost:8345;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
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 notfel
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /var/notfellchen/static/;
|
alias /var/ilmo/static/;
|
||||||
access_log off;
|
access_log off;
|
||||||
expires 365d;
|
expires 365d;
|
||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
@ -230,22 +230,22 @@ We recommend reading about setting `strong encryption settings`_ for your web se
|
|||||||
Next steps
|
Next steps
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Yay, you are done! You should now be able to reach notfellchen at https://notfellchen.example.com/
|
Yay, you are done! You should now be able to reach ilmo at https://ilmo.example.com/
|
||||||
|
|
||||||
Updates
|
Updates
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
||||||
|
|
||||||
To upgrade to a new notfellchen release, pull the latest code changes and run the following commands::
|
To upgrade to a new ilmo release, pull the latest code changes and run the following commands::
|
||||||
|
|
||||||
$ source /var/notfellchen/venv/bin/activate
|
$ source /var/ilmo/venv/bin/activate
|
||||||
(venv)$ git pull
|
(venv)$ git pull
|
||||||
(venv)$ pg_dump notfellchen > notfellchen.psql
|
(venv)$ pg_dump ilmo > ilmo.psql
|
||||||
(venv)$ python manage.py migrate
|
(venv)$ python manage.py migrate
|
||||||
(venv)$ django-admin compilemessages --ignore venv
|
(venv)$ django-admin compilemessages --ignore venv
|
||||||
|
|
||||||
# systemctl restart notfellchen-web
|
# systemctl restart ilmo-web
|
||||||
|
|
||||||
|
|
||||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||||
|
@ -8,6 +8,5 @@ Installation, customization and contributing
|
|||||||
|
|
||||||
deployment.rst
|
deployment.rst
|
||||||
contributing.rst
|
contributing.rst
|
||||||
translation.rst
|
|
||||||
release.rst
|
release.rst
|
||||||
backup.rst
|
backup.rst
|
||||||
|
@ -12,7 +12,10 @@ Notfellchen Plattform Dokumentation
|
|||||||
API/index.rst
|
API/index.rst
|
||||||
|
|
||||||
.. image:: rtfm.png
|
.. image:: rtfm.png
|
||||||
:name: Ratte lesend
|
:name: RTFM by Elektroll
|
||||||
:alt: Zeichnung einer lesenden Ratte
|
:scale: 50 %
|
||||||
|
:alt: Soviet style image of workers holding a sign with a gear and a screwdriver. Below is says "Read the manual"
|
||||||
:align: center
|
: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: 485 KiB After Width: | Height: | Size: 815 KiB |
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
@ -1,19 +1,9 @@
|
|||||||
Benachrichtigungen
|
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
|
E-Mail
|
||||||
++++++
|
++++++
|
||||||
|
|
||||||
Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an.
|
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 @@
|
|||||||
******************
|
***********
|
||||||
User Dokumentation
|
Users guide
|
||||||
******************
|
***********
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Inhalt:
|
:caption: Contents:
|
||||||
|
|
||||||
registrierung.rst
|
registrierung.rst
|
||||||
vermittlungen.rst
|
|
||||||
moderationskonzept.rst
|
|
||||||
benachrichtigungen.rst
|
benachrichtigungen.rst
|
||||||
|
login.rst
|
||||||
|
email.rst
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
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,10 +1,5 @@
|
|||||||
Registrierung
|
Registration
|
||||||
================================
|
================================
|
||||||
|
|
||||||
Du kannst dich jederzeit selbst registrieren. Das geht unter https://notfellchen.org/accounts/register/
|
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.
|
||||||
Ein Account ermöglicht es dir
|
|
||||||
|
|
||||||
* Kommentare zu hinterlassen
|
|
||||||
* Vermittlungen hinzuzufügen
|
|
||||||
* Vermittlungen zu abonnieren
|
|
||||||
|
@ -1,17 +1,3 @@
|
|||||||
Vermittlungen
|
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.
|
|
||||||
|
@ -24,7 +24,4 @@ console-only=true
|
|||||||
app_log_level=INFO
|
app_log_level=INFO
|
||||||
django_log_level=INFO
|
django_log_level=INFO
|
||||||
|
|
||||||
[geocoding]
|
|
||||||
api_url=https://photon.hyteck.de/api
|
|
||||||
api_format=photon
|
|
||||||
|
|
||||||
|
@ -38,18 +38,10 @@ dependencies = [
|
|||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"django-crispy-forms",
|
"django-crispy-forms",
|
||||||
"crispy-bootstrap4",
|
"crispy-bootstrap4",
|
||||||
"djangorestframework",
|
"djangorestframework"
|
||||||
"celery[redis]",
|
|
||||||
"drf-spectacular[sidecar]"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
dynamic = ["version", "readme"]
|
dynamic = ["version", "readme"]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
develop = [
|
|
||||||
"pytest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://notfellchen.org"
|
homepage = "https://notfellchen.org"
|
||||||
repository = "https://codeberg.org/moanos/notfellchen/"
|
repository = "https://codeberg.org/moanos/notfellchen/"
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import csv
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.utils.html import format_html
|
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, SearchSubscription, \
|
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice
|
||||||
SpeciesSpecificURL
|
|
||||||
|
|
||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
|
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class StatusInline(admin.StackedInline):
|
class StatusInline(admin.StackedInline):
|
||||||
@ -20,52 +14,13 @@ class StatusInline(admin.StackedInline):
|
|||||||
|
|
||||||
@admin.register(AdoptionNotice)
|
@admin.register(AdoptionNotice)
|
||||||
class AdoptionNoticeAdmin(admin.ModelAdmin):
|
class AdoptionNoticeAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("name__icontains", "description__icontains")
|
|
||||||
list_filter = ("owner",)
|
|
||||||
inlines = [
|
inlines = [
|
||||||
StatusInline,
|
StatusInline,
|
||||||
]
|
]
|
||||||
actions = ("activate",)
|
|
||||||
|
|
||||||
def activate(self, request, queryset):
|
|
||||||
for obj in queryset:
|
|
||||||
obj.set_active()
|
|
||||||
|
|
||||||
activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
|
|
||||||
|
|
||||||
|
|
||||||
# Re-register UserAdmin
|
# Re-register UserAdmin
|
||||||
@admin.register(User)
|
admin.site.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):
|
def _reported_content_link(obj):
|
||||||
@ -94,48 +49,16 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
reported_content_link.short_description = "Reported Content"
|
reported_content_link.short_description = "Reported Content"
|
||||||
|
|
||||||
class SpeciesSpecificURLInline(admin.StackedInline):
|
|
||||||
model = SpeciesSpecificURL
|
|
||||||
|
|
||||||
@admin.register(RescueOrganization)
|
|
||||||
class RescueOrganizationAdmin(admin.ModelAdmin):
|
|
||||||
search_fields = ("name","description", "internal_comment", "location_string")
|
|
||||||
list_display = ("name", "trusted", "allows_using_materials", "website")
|
|
||||||
list_filter = ("allows_using_materials", "trusted",)
|
|
||||||
|
|
||||||
inlines = [
|
|
||||||
SpeciesSpecificURLInline,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@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.register(BaseNotification)
|
|
||||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = ("user", "read")
|
|
||||||
|
|
||||||
@admin.register(SearchSubscription)
|
|
||||||
class SearchSubscriptionAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = ("owner",)
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Animal)
|
admin.site.register(Animal)
|
||||||
admin.site.register(Species)
|
admin.site.register(Species)
|
||||||
|
admin.site.register(RescueOrganization)
|
||||||
admin.site.register(Location)
|
admin.site.register(Location)
|
||||||
admin.site.register(Rule)
|
admin.site.register(Rule)
|
||||||
admin.site.register(Image)
|
admin.site.register(Image)
|
||||||
admin.site.register(ModerationAction)
|
admin.site.register(ModerationAction)
|
||||||
admin.site.register(Language)
|
admin.site.register(Language)
|
||||||
|
admin.site.register(Text)
|
||||||
admin.site.register(Announcement)
|
admin.site.register(Announcement)
|
||||||
admin.site.register(AdoptionNoticeStatus)
|
admin.site.register(AdoptionNoticeStatus)
|
||||||
admin.site.register(Subscriptions)
|
admin.site.register(Subscriptions)
|
||||||
admin.site.register(Log)
|
|
||||||
admin.site.register(Timestamp)
|
|
||||||
|
@ -1,53 +1,10 @@
|
|||||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
from ..models import AdoptionNotice
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdoptionNotice
|
model = AdoptionNotice
|
||||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", "group_only"]
|
||||||
"group_only"]
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalCreateSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Animal
|
|
||||||
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
|
||||||
|
|
||||||
class RescueOrgSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = RescueOrganization
|
|
||||||
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
|
|
||||||
"website", "description", "external_object_identifier", "external_source_identifier"]
|
|
||||||
|
|
||||||
class AnimalGetSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Animal
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganizationSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = RescueOrganization
|
|
||||||
exclude = ["internal_comment", "allows_using_materials"]
|
|
||||||
|
|
||||||
|
|
||||||
class ImageCreateSerializer(serializers.ModelSerializer):
|
|
||||||
@staticmethod
|
|
||||||
def _animal_or_an(value):
|
|
||||||
if not value in ["animal", "adoption_notice"]:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
'Set either animal or adoption_notice, depending on what type of object the image should be attached to.')
|
|
||||||
|
|
||||||
attach_to_type = serializers.CharField(validators=[_animal_or_an])
|
|
||||||
attach_to = serializers.IntegerField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Image
|
|
||||||
exclude = ["owner"]
|
|
||||||
|
|
||||||
|
|
||||||
class SpeciesSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Species
|
|
||||||
fields = "__all__"
|
|
@ -1,16 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import (
|
from .views import (
|
||||||
AdoptionNoticeApiView,
|
AdoptionNoticeApiView
|
||||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
|
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
|
path('adoption_notice', AdoptionNoticeApiView.as_view()),
|
||||||
path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),
|
|
||||||
path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
|
|
||||||
path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
|
|
||||||
path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
|
|
||||||
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
|
|
||||||
path("images/", AddImageApiView.as_view(), name="api-add-image"),
|
|
||||||
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
|
|
||||||
]
|
]
|
||||||
|
@ -1,212 +1,37 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db import transaction
|
|
||||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
|
|
||||||
from fellchensammlung.tasks import post_adoption_notice_save
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework import permissions
|
||||||
from .serializers import (
|
from ..models import AdoptionNotice
|
||||||
AnimalGetSerializer,
|
from .serializers import AdoptionNoticeSerializer
|
||||||
AnimalCreateSerializer,
|
|
||||||
RescueOrganizationSerializer,
|
|
||||||
AdoptionNoticeSerializer,
|
|
||||||
ImageCreateSerializer,
|
|
||||||
SpeciesSerializer, RescueOrgSerializer,
|
|
||||||
)
|
|
||||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
|
||||||
from drf_spectacular.utils import extend_schema
|
|
||||||
|
|
||||||
class AdoptionNoticeApiView(APIView):
|
class AdoptionNoticeApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(
|
|
||||||
parameters=[
|
|
||||||
{
|
|
||||||
'name': 'id',
|
|
||||||
'required': False,
|
|
||||||
'description': 'ID of the adoption notice to retrieve.',
|
|
||||||
'type': int
|
|
||||||
},
|
|
||||||
],
|
|
||||||
responses={200: AdoptionNoticeSerializer(many=True)}
|
|
||||||
)
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
serializer_context = {
|
||||||
Retrieve adoption notices with their related animals and images.
|
'request': request,
|
||||||
"""
|
}
|
||||||
adoption_notice_id = kwargs.get("id")
|
|
||||||
if adoption_notice_id:
|
|
||||||
try:
|
|
||||||
adoption_notice = AdoptionNotice.objects.get(pk=adoption_notice_id)
|
|
||||||
serializer = AdoptionNoticeSerializer(adoption_notice, context={"request": request})
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except AdoptionNotice.DoesNotExist:
|
|
||||||
return Response({"error": "Adoption notice not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
adoption_notices = AdoptionNotice.objects.all()
|
adoption_notices = AdoptionNotice.objects.all()
|
||||||
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
|
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context=serializer_context)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
@extend_schema(
|
|
||||||
request=AdoptionNoticeSerializer,
|
|
||||||
responses={201: 'Adoption notice created successfully!'}
|
|
||||||
)
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
data = {
|
||||||
API view to add an adoption notice.
|
'name': request.data.get('name'),
|
||||||
"""
|
"searching_since": request.data.get('searching_since'),
|
||||||
serializer = AdoptionNoticeSerializer(data=request.data, context={'request': request})
|
"description": request.data.get('description'),
|
||||||
if not serializer.is_valid():
|
"organization": request.data.get('organization'),
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
"further_information": request.data.get('further_information'),
|
||||||
|
"location_string": request.data.get('location_string'),
|
||||||
adoption_notice = serializer.save(owner=request.user)
|
"group_only": request.data.get('group_only'),
|
||||||
|
"owner": request.data.get('owner')
|
||||||
# Add the location
|
}
|
||||||
post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
|
serializer = AdoptionNoticeSerializer(data=data)
|
||||||
|
|
||||||
# Only set active when user has trust level moderator or higher
|
|
||||||
if request.user.trust_level >= TrustLevel.MODERATOR:
|
|
||||||
adoption_notice.set_active()
|
|
||||||
else:
|
|
||||||
adoption_notice.set_unchecked()
|
|
||||||
|
|
||||||
# Log the action
|
|
||||||
Log.objects.create(
|
|
||||||
user=request.user,
|
|
||||||
action="add_adoption_notice",
|
|
||||||
text=f"{request.user} added adoption notice {adoption_notice.pk} via API",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return success response with new adoption notice details
|
|
||||||
return Response(
|
|
||||||
{"message": "Adoption notice created successfully!", "id": adoption_notice.pk},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalApiView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Get list of animals or a specific animal by ID.
|
|
||||||
"""
|
|
||||||
animal_id = kwargs.get("id")
|
|
||||||
if animal_id:
|
|
||||||
try:
|
|
||||||
animal = Animal.objects.get(pk=animal_id)
|
|
||||||
serializer = AnimalGetSerializer(animal, context={"request": request})
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except Animal.DoesNotExist:
|
|
||||||
return Response({"error": "Animal not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
animals = Animal.objects.all()
|
|
||||||
serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new animal.
|
|
||||||
"""
|
|
||||||
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
animal = serializer.save(owner=request.user)
|
serializer.save()
|
||||||
return Response(
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
{"message": "Animal created successfully!", "id": animal.id},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
class RescueOrganizationApiView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
@extend_schema(
|
|
||||||
parameters=[
|
|
||||||
{
|
|
||||||
'name': 'id',
|
|
||||||
'required': False,
|
|
||||||
'description': 'ID of the rescue organization to retrieve.',
|
|
||||||
'type': int
|
|
||||||
},
|
|
||||||
],
|
|
||||||
responses={200: RescueOrganizationSerializer(many=True)}
|
|
||||||
)
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Get list of rescue organizations or a specific organization by ID.
|
|
||||||
"""
|
|
||||||
org_id = kwargs.get("id")
|
|
||||||
if org_id:
|
|
||||||
try:
|
|
||||||
organization = RescueOrganization.objects.get(pk=org_id)
|
|
||||||
serializer = RescueOrganizationSerializer(organization, context={"request": request})
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
except RescueOrganization.DoesNotExist:
|
|
||||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
organizations = RescueOrganization.objects.all()
|
|
||||||
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
@extend_schema(
|
|
||||||
request=RescueOrgSerializer, # Document the request body
|
|
||||||
responses={201: 'Rescue organization created/updated successfully!'}
|
|
||||||
)
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Create or update a rescue organization.
|
|
||||||
"""
|
|
||||||
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
|
|
||||||
if serializer.is_valid():
|
|
||||||
rescue_org = serializer.save(owner=request.user)
|
|
||||||
return Response(
|
|
||||||
{"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
class AddImageApiView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
@extend_schema(
|
|
||||||
request=ImageCreateSerializer,
|
|
||||||
responses={201: 'Image added successfully!'}
|
|
||||||
)
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Add an image to an animal or adoption notice.
|
|
||||||
"""
|
|
||||||
serializer = ImageCreateSerializer(data=request.data, context={"request": request})
|
|
||||||
if serializer.is_valid():
|
|
||||||
if serializer.validated_data["attach_to_type"] == "animal":
|
|
||||||
object_to_attach_to = Animal.objects.get(id=serializer.validated_data["attach_to"])
|
|
||||||
elif serializer.validated_data["attach_to_type"] == "adoption_notice":
|
|
||||||
object_to_attach_to = AdoptionNotice.objects.get(id=serializer.validated_data["attach_to"])
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
|
|
||||||
serializer.validated_data.pop('attach_to_type', None)
|
|
||||||
serializer.validated_data.pop('attach_to', None)
|
|
||||||
image = serializer.save(owner=request.user)
|
|
||||||
object_to_attach_to.photos.add(image)
|
|
||||||
return Response(
|
|
||||||
{"message": "Image added successfully!", "id": image.id},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
class SpeciesApiView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
@extend_schema(
|
|
||||||
responses={200: SpeciesSerializer(many=True)}
|
|
||||||
)
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Retrieve a list of species.
|
|
||||||
"""
|
|
||||||
species = Species.objects.all()
|
|
||||||
serializer = SpeciesSerializer(species, many=True, context={"request": request})
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
||||||
|
@ -15,4 +15,3 @@ class FellchensammlungConfig(AppConfig):
|
|||||||
except Permission.DoesNotExist:
|
except Permission.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
post_migrate.connect(ensure_languages, sender=self)
|
post_migrate.connect(ensure_languages, sender=self)
|
||||||
import fellchensammlung.receivers
|
|
||||||
|
@ -1,21 +1,14 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||||
Comment, SexChoicesWithAll, DistanceChoices
|
Comment
|
||||||
from django_registration.forms import RegistrationForm
|
from django_registration.forms import RegistrationForm
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
|
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from notfellchen.settings import MEDIA_URL
|
from notfellchen.settings import MEDIA_URL
|
||||||
from crispy_forms.layout import Div
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
class DateInput(forms.DateInput):
|
||||||
@ -52,7 +45,6 @@ class AdoptionNoticeForm(forms.ModelForm):
|
|||||||
'group_only',
|
'group_only',
|
||||||
'searching_since',
|
'searching_since',
|
||||||
'location_string',
|
'location_string',
|
||||||
'organization',
|
|
||||||
'description',
|
'description',
|
||||||
'further_information',
|
'further_information',
|
||||||
),
|
),
|
||||||
@ -60,20 +52,19 @@ class AdoptionNoticeForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdoptionNotice
|
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 AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdoptionNotice
|
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 = {
|
widgets = {
|
||||||
'searching_since': DateInput(),
|
'searching_since': DateInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalForm(forms.ModelForm):
|
class AnimalForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||||
@ -102,7 +93,6 @@ class AnimalFormWithDateWidget(AnimalForm):
|
|||||||
'date_of_birth': DateInput(),
|
'date_of_birth': DateInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
|
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
|
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
|
||||||
@ -125,21 +115,11 @@ class ImageForm(forms.ModelForm):
|
|||||||
self.helper.form_id = 'form-animal-photo'
|
self.helper.form_id = 'form-animal-photo'
|
||||||
self.helper.form_class = 'card'
|
self.helper.form_class = 'card'
|
||||||
self.helper.form_method = 'post'
|
self.helper.form_method = 'post'
|
||||||
|
|
||||||
if in_flow:
|
if in_flow:
|
||||||
submits= Div(Submit('submit', _('Speichern')),
|
self.helper.add_input(Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')))
|
||||||
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
|
self.helper.add_input(Submit('submit', _('Speichern')))
|
||||||
else:
|
else:
|
||||||
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
|
self.helper.add_input(Submit('submit', _('Submit')))
|
||||||
self.helper.layout = Layout(
|
|
||||||
Div(
|
|
||||||
'image',
|
|
||||||
'alt_text',
|
|
||||||
css_class="spaced",
|
|
||||||
),
|
|
||||||
submits
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Image
|
model = Image
|
||||||
@ -162,8 +142,8 @@ class CommentForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_action = "comment"
|
||||||
self.helper.form_class = 'form-comments'
|
self.helper.form_class = 'form-comments'
|
||||||
self.helper.add_input(Hidden('action', 'comment'))
|
|
||||||
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -181,8 +161,6 @@ class CustomRegistrationForm(RegistrationForm):
|
|||||||
class Meta(RegistrationForm.Meta):
|
class Meta(RegistrationForm.Meta):
|
||||||
model = User
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
@ -192,8 +170,10 @@ class CustomRegistrationForm(RegistrationForm):
|
|||||||
self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
|
self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_distances():
|
||||||
|
return {i: i for i in [10, 20, 50, 100, 200, 500]}
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeSearchForm(forms.Form):
|
class AdoptionNoticeSearchForm(forms.Form):
|
||||||
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
postcode = forms.CharField(max_length=20, label=_("Postleitzahl"))
|
||||||
initial=SexChoicesWithAll.ALL)
|
max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
|
||||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
|
|
||||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
from django.db.models.signals import post_save
|
import django.conf.global_settings
|
||||||
from django.dispatch import receiver
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.translation import gettext
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
|
from django.db.models import Q, Min
|
||||||
|
from fellchensammlung.models import User
|
||||||
from notfellchen.settings import host
|
from notfellchen.settings import host
|
||||||
|
|
||||||
NEWLINE = "\r\n"
|
NEWLINE = "\r\n"
|
||||||
|
|
||||||
|
|
||||||
def mail_admins_new_report(report):
|
def mail_admins_new_report(report):
|
||||||
subject = _("Neue Meldung")
|
subject = _("Neue Meldung")
|
||||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
|
||||||
greeting = _("Moin,") + "{NEWLINE}"
|
greeting = _("Moin,") + "{NEWLINE}"
|
||||||
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
|
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
|
||||||
if len(report.reported_broken_rules.all()) > 0:
|
if len(report.reported_broken_rules.all()) > 0:
|
||||||
@ -29,15 +29,5 @@ def mail_admins_new_report(report):
|
|||||||
link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
|
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
|
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])
|
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
|
||||||
|
print("Sending email to ", moderator.email)
|
||||||
message.send()
|
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 django.core.management import BaseCommand
|
||||||
from fellchensammlung.models import AdoptionNotice, Location
|
from fellchensammlung.models import AdoptionNotice, Location
|
||||||
from fellchensammlung.tools.admin import clean_locations
|
from fellchensammlung.tools.geo import clean_locations
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -7,7 +7,7 @@ from fellchensammlung import baker_recipes
|
|||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \
|
from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \
|
||||||
Report, Comment, ReportAdoptionNotice, TrustLevel
|
Report, Comment, ReportAdoptionNotice
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -101,10 +101,10 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
User.objects.create_user('test', password='foobar')
|
User.objects.create_user('test', password='foobar')
|
||||||
admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org",
|
admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org",
|
||||||
trust_level=TrustLevel.ADMIN)
|
trust_level=User.TRUST_LEVEL[User.ADMIN])
|
||||||
|
|
||||||
mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org",
|
mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org",
|
||||||
trust_level=TrustLevel.MODERATOR)
|
trust_level=User.TRUST_LEVEL[User.MODERATOR])
|
||||||
|
|
||||||
comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1)
|
comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1)
|
||||||
comment2 = baker.make(Comment,
|
comment2 = baker.make(Comment,
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,25 +0,0 @@
|
|||||||
# 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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,21 +0,0 @@
|
|||||||
# 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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,24 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,136 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,20 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,19 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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',
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,19 +0,0 @@
|
|||||||
# 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,
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.1 on 2024-11-21 19:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0023_user_email_notifications'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='animal',
|
|
||||||
name='sex',
|
|
||||||
field=models.CharField(choices=[('M_N', 'neutered male'), ('M', 'male'), ('F_N', 'neutered female'), ('F', 'female'), ('I', 'intersex')], max_length=20),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.1 on 2024-11-21 19:41
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0024_alter_animal_sex'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='animal',
|
|
||||||
name='sex',
|
|
||||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intersex')], max_length=20),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.1 on 2024-11-21 21:49
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0025_alter_animal_sex'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='animal',
|
|
||||||
name='sex',
|
|
||||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 5.1.1 on 2024-12-14 07:57
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0026_alter_animal_sex'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='animal',
|
|
||||||
name='species',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.species', verbose_name='Tierart'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AndoptionNoticeNotification',
|
|
||||||
fields=[
|
|
||||||
('basenotification_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fellchensammlung.basenotification')),
|
|
||||||
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
|
|
||||||
],
|
|
||||||
bases=('fellchensammlung.basenotification',),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.1.1 on 2024-12-26 15:56
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0027_alter_animal_species_andoptionnoticenotification'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SearchSubscription',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20)),
|
|
||||||
('radius', models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')])),
|
|
||||||
('location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2024-12-31 10:37
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0028_searchsubscription'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameModel(
|
|
||||||
old_name='AndoptionNoticeNotification',
|
|
||||||
new_name='AdoptionNoticeNotification',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='searchsubscription',
|
|
||||||
name='sex',
|
|
||||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2024-12-31 12:23
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0029_rename_andoptionnoticenotification_adoptionnoticenotification_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name='searchsubscription',
|
|
||||||
old_name='radius',
|
|
||||||
new_name='max_distance',
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2024-12-31 12:42
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0030_rename_radius_searchsubscription_max_distance'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='searchsubscription',
|
|
||||||
name='location',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='searchsubscription',
|
|
||||||
name='max_distance',
|
|
||||||
field=models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')], null=True),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-01-01 22:04
|
|
||||||
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0031_alter_searchsubscription_location_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='searchsubscription',
|
|
||||||
name='created_at',
|
|
||||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='searchsubscription',
|
|
||||||
name='updated_at',
|
|
||||||
field=models.DateTimeField(auto_now=True),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-01-05 18:18
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0032_searchsubscription_created_at_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rescueorganization',
|
|
||||||
name='external_object_identifier',
|
|
||||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='External Object Identifier'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='rescueorganization',
|
|
||||||
name='external_source_identifier',
|
|
||||||
field=models.CharField(blank=True, choices=[('OSM', 'Open Street Map')], max_length=200, null=True, verbose_name='External Source Identifier'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-01-05 19:36
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0033_rescueorganization_external_object_identifier_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='SpeciesSpecificURL',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('url', models.URLField(verbose_name='Tierartspezifische URL')),
|
|
||||||
('rescues_organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.rescueorganization', verbose_name='Tierschutzorganisation')),
|
|
||||||
('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.species', verbose_name='Tierart')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-01-11 12:58
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('fellchensammlung', '0034_speciesspecificurl'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='image',
|
|
||||||
name='alt_text',
|
|
||||||
field=models.TextField(max_length=2000, verbose_name='Alternativtext'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='report',
|
|
||||||
name='reported_broken_rules',
|
|
||||||
field=models.ManyToManyField(to='fellchensammlung.rule', verbose_name='Regeln gegen die verstoßen wurde'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='report',
|
|
||||||
name='user_comment',
|
|
||||||
field=models.TextField(blank=True, verbose_name='Kommentar/Zusätzliche Information'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,10 +1,9 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from random import choices
|
|
||||||
from tabnanny import verbose
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
@ -13,8 +12,6 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
|
|
||||||
from .tools import misc, geo
|
from .tools import misc, geo
|
||||||
from notfellchen.settings import MEDIA_URL
|
from notfellchen.settings import MEDIA_URL
|
||||||
from .tools.geo import LocationProxy, Position
|
|
||||||
from .tools.misc import age_as_hr_string, time_since_as_hr_string
|
|
||||||
|
|
||||||
|
|
||||||
class Language(models.Model):
|
class Language(models.Model):
|
||||||
@ -38,55 +35,105 @@ class Language(models.Model):
|
|||||||
verbose_name_plural = _('Sprachen')
|
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):
|
class Location(models.Model):
|
||||||
place_id = models.IntegerField() # OSM id
|
place_id = models.IntegerField()
|
||||||
latitude = models.FloatField()
|
latitude = models.FloatField()
|
||||||
longitude = models.FloatField()
|
longitude = models.FloatField()
|
||||||
name = models.CharField(max_length=2000)
|
name = models.CharField(max_length=2000)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self):
|
|
||||||
return (self.latitude, self.longitude)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def str_hr(self):
|
|
||||||
return f"{self.name.split(',')[0]}"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_location_from_string(location_string):
|
def get_location_from_string(location_string):
|
||||||
try:
|
geo_api = geo.GeoAPI()
|
||||||
proxy = LocationProxy(location_string)
|
geojson = geo_api.get_geojson_for_query(location_string)
|
||||||
except ValueError:
|
if geojson is None:
|
||||||
return None
|
return None
|
||||||
location = Location.get_location_from_proxy(proxy)
|
result = geojson[0]
|
||||||
return location
|
if "name" in result:
|
||||||
|
name = result["name"]
|
||||||
@staticmethod
|
else:
|
||||||
def get_location_from_proxy(proxy):
|
name = result["display_name"]
|
||||||
location = Location.objects.create(
|
location = Location.objects.create(
|
||||||
place_id=proxy.place_id,
|
place_id=result["place_id"],
|
||||||
latitude=proxy.latitude,
|
latitude=result["lat"],
|
||||||
longitude=proxy.longitude,
|
longitude=result["lon"],
|
||||||
name=proxy.name,
|
name=name,
|
||||||
)
|
)
|
||||||
return location
|
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 ExternalSourceChoices(models.TextChoices):
|
|
||||||
OSM = "OSM", _("Open Street Map")
|
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganization(models.Model):
|
class RescueOrganization(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -108,142 +155,13 @@ class RescueOrganization(models.Model):
|
|||||||
|
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
||||||
allows_using_materials = models.CharField(max_length=200,
|
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'))
|
||||||
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_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
||||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
||||||
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
||||||
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
|
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
|
||||||
fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse 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'))
|
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
|
|
||||||
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
|
||||||
verbose_name=_('External Object Identifier'))
|
|
||||||
external_source_identifier = models.CharField(max_length=200, null=True, blank=True,
|
|
||||||
choices=ExternalSourceChoices.choices,
|
|
||||||
verbose_name=_('External Source Identifier'))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self):
|
|
||||||
if self.location:
|
|
||||||
return Position(latitude=self.location.latitude, longitude=self.location.longitude)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description_short(self):
|
|
||||||
if self.description is None:
|
|
||||||
return ""
|
|
||||||
if len(self.description) > 200:
|
|
||||||
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
|
||||||
|
|
||||||
|
|
||||||
# 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_full_name(self):
|
|
||||||
if self.first_name and self.last_name:
|
|
||||||
return self.first_name + self.last_name
|
|
||||||
else:
|
|
||||||
return self.username
|
|
||||||
|
|
||||||
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_unread_notifications(self):
|
|
||||||
return BaseNotification.objects.filter(user=self, read=False)
|
|
||||||
|
|
||||||
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, verbose_name=_('Alternativtext'))
|
|
||||||
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):
|
class AdoptionNotice(models.Model):
|
||||||
@ -253,13 +171,10 @@ class AdoptionNotice(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
return f"{self.name}"
|
||||||
return self.name
|
|
||||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
|
||||||
|
|
||||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
created_at = models.DateField(verbose_name=_('Erstellt am'), default=datetime.now)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=datetime.now)
|
||||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
|
||||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||||
@ -276,31 +191,6 @@ class AdoptionNotice(models.Model):
|
|||||||
def animals(self):
|
def animals(self):
|
||||||
return Animal.objects.filter(adoption_notice=self)
|
return Animal.objects.filter(adoption_notice=self)
|
||||||
|
|
||||||
@property
|
|
||||||
def sexes(self):
|
|
||||||
sexes = set()
|
|
||||||
for animal in self.animals:
|
|
||||||
sexes.add(animal.sex)
|
|
||||||
return sexes
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_checked_hr(self):
|
|
||||||
time_since_last_checked = timezone.now() - self.last_checked
|
|
||||||
return time_since_as_hr_string(time_since_last_checked)
|
|
||||||
|
|
||||||
def sex_code(self):
|
|
||||||
# Treat Intersex as mixed in order to increase their visibility
|
|
||||||
if len(self.sexes) > 1:
|
|
||||||
return "mixed"
|
|
||||||
|
|
||||||
sex = self.sexes.pop()
|
|
||||||
if sex == SexChoices.MALE:
|
|
||||||
return "male"
|
|
||||||
elif sex == SexChoices.FEMALE:
|
|
||||||
return "female"
|
|
||||||
else:
|
|
||||||
return "mixed"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def comments(self):
|
def comments(self):
|
||||||
return Comment.objects.filter(adoption_notice=self)
|
return Comment.objects.filter(adoption_notice=self)
|
||||||
@ -331,11 +221,6 @@ class AdoptionNotice(models.Model):
|
|||||||
# returns all subscriptions to that adoption notice
|
# returns all subscriptions to that adoption notice
|
||||||
return Subscriptions.objects.filter(adoption_notice=self)
|
return Subscriptions.objects.filter(adoption_notice=self)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_active_ANs():
|
|
||||||
active_ans = [an for an in AdoptionNotice.objects.all() if an.is_active]
|
|
||||||
return active_ans
|
|
||||||
|
|
||||||
def get_photos(self):
|
def get_photos(self):
|
||||||
"""
|
"""
|
||||||
First trys to get group photos that are attached to the adoption notice if there is none it trys to fetch
|
First trys to get group photos that are attached to the adoption notice if there is none it trys to fetch
|
||||||
@ -392,36 +277,14 @@ class AdoptionNotice(models.Model):
|
|||||||
return False
|
return False
|
||||||
return self.adoptionnoticestatus.is_active
|
return self.adoptionnoticestatus.is_active
|
||||||
|
|
||||||
@property
|
def set_checked(self):
|
||||||
def is_disabled_unchecked(self):
|
self.last_checked = datetime.now()
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
self.save()
|
||||||
return False
|
|
||||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
|
||||||
|
|
||||||
def set_closed(self):
|
def set_closed(self):
|
||||||
self.last_checked = timezone.now()
|
self.last_checked = datetime.now()
|
||||||
self.save()
|
|
||||||
self.adoptionnoticestatus.set_closed()
|
self.adoptionnoticestatus.set_closed()
|
||||||
|
|
||||||
def set_active(self):
|
|
||||||
self.last_checked = timezone.now()
|
|
||||||
self.save()
|
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
|
||||||
AdoptionNoticeStatus.create_other(self)
|
|
||||||
self.adoptionnoticestatus.set_active()
|
|
||||||
|
|
||||||
def set_unchecked(self):
|
|
||||||
self.last_checked = timezone.now()
|
|
||||||
self.save()
|
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
|
||||||
AdoptionNoticeStatus.create_other(self)
|
|
||||||
self.adoptionnoticestatus.set_unchecked()
|
|
||||||
|
|
||||||
for subscription in self.get_subscriptions():
|
|
||||||
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
|
|
||||||
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
|
|
||||||
BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title)
|
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeStatus(models.Model):
|
class AdoptionNoticeStatus(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -461,7 +324,6 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
"against_the_rules": "against_the_rules",
|
"against_the_rules": "against_the_rules",
|
||||||
"missing_information": "missing_information",
|
"missing_information": "missing_information",
|
||||||
"technical_error": "technical_error",
|
"technical_error": "technical_error",
|
||||||
"unchecked": "unchecked",
|
|
||||||
"other": "other"
|
"other": "other"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -472,90 +334,51 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
minor_choices.update(MINOR_STATUS_CHOICES[key])
|
minor_choices.update(MINOR_STATUS_CHOICES[key])
|
||||||
minor_status = models.CharField(choices=minor_choices, max_length=200)
|
minor_status = models.CharField(choices=minor_choices, max_length=200)
|
||||||
adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
|
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):
|
def __str__(self):
|
||||||
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
|
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
|
||||||
|
|
||||||
def as_string(self):
|
|
||||||
return f"{self.major_status}, {self.minor_status}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
return self.major_status == self.ACTIVE
|
return self.major_status == self.ACTIVE
|
||||||
|
|
||||||
@property
|
|
||||||
def is_disabled_unchecked(self):
|
|
||||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_minor_choices(major_status):
|
def get_minor_choices(major_status):
|
||||||
return AdoptionNoticeStatus.MINOR_STATUS_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):
|
def set_closed(self):
|
||||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||||
self.save()
|
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 SexChoices(models.TextChoices):
|
|
||||||
FEMALE = "F", _("Weiblich")
|
|
||||||
MALE = "M", _("Männlich")
|
|
||||||
MALE_NEUTERED = "M_N", _("Männlich, kastriert")
|
|
||||||
FEMALE_NEUTERED = "F_N", _("Weiblich, kastriert")
|
|
||||||
INTER = "I", _("Intergeschlechtlich")
|
|
||||||
|
|
||||||
|
|
||||||
class SexChoicesWithAll(models.TextChoices):
|
|
||||||
FEMALE = "F", _("Weiblich")
|
|
||||||
MALE = "M", _("Männlich")
|
|
||||||
MALE_NEUTERED = "M_N", _("Männlich, kastriert")
|
|
||||||
FEMALE_NEUTERED = "F_N", _("Weiblich Kastriert")
|
|
||||||
INTER = "I", _("Intergeschlechtlich")
|
|
||||||
ALL = "A", _("Alle")
|
|
||||||
|
|
||||||
|
|
||||||
class Animal(models.Model):
|
class Animal(models.Model):
|
||||||
|
MALE_NEUTERED = "M_N"
|
||||||
|
MALE = "M"
|
||||||
|
FEMALE_NEUTERED = "F_N"
|
||||||
|
FEMALE = "F"
|
||||||
|
SEX_CHOICES = {
|
||||||
|
MALE_NEUTERED: "neutered male",
|
||||||
|
MALE: "male",
|
||||||
|
FEMALE_NEUTERED: "neutered female",
|
||||||
|
FEMALE: "female",
|
||||||
|
}
|
||||||
|
|
||||||
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||||
species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart"))
|
species = models.ForeignKey(Species, on_delete=models.PROTECT)
|
||||||
photos = models.ManyToManyField(Image, blank=True)
|
photos = models.ManyToManyField(Image, blank=True)
|
||||||
sex = models.CharField(
|
sex = models.CharField(max_length=20, choices=SEX_CHOICES, )
|
||||||
max_length=20,
|
|
||||||
choices=SexChoices.choices,
|
|
||||||
)
|
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
||||||
owner = models.ForeignKey(User, 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):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def age(self):
|
def age(self):
|
||||||
return timezone.now().today().date() - self.date_of_birth
|
return datetime.today().date() - self.date_of_birth
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hr_age(self):
|
def hr_age(self):
|
||||||
@ -581,40 +404,6 @@ class Animal(models.Model):
|
|||||||
return reverse('animal-detail', args=[str(self.id)])
|
return reverse('animal-detail', args=[str(self.id)])
|
||||||
|
|
||||||
|
|
||||||
class DistanceChoices(models.IntegerChoices):
|
|
||||||
TWENTY = 20, '20 km'
|
|
||||||
FIFTY = 50, '50 km'
|
|
||||||
ONE_HUNDRED = 100, '100 km'
|
|
||||||
TWO_HUNDRED = 200, '200 km'
|
|
||||||
FIVE_HUNDRED = 500, '500 km'
|
|
||||||
|
|
||||||
|
|
||||||
class SearchSubscription(models.Model):
|
|
||||||
"""
|
|
||||||
SearchSubscriptions allow a user to get a notification when a new AdoptionNotice is added that matches their Search
|
|
||||||
criteria. Search criteria are location, SexChoicesWithAll and distance
|
|
||||||
|
|
||||||
Process:
|
|
||||||
- User performs a normal search
|
|
||||||
- User clicks Button "Subscribe to this Search"
|
|
||||||
- SearchSubscription is added to database
|
|
||||||
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
|
|
||||||
- For matches: Send notification to user of the SearchSubscription
|
|
||||||
"""
|
|
||||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
||||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
|
|
||||||
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
|
|
||||||
max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.location and self.max_distance:
|
|
||||||
return f"{self.owner}: [{SexChoicesWithAll(self.sex).label}] {self.max_distance}km - {self.location}"
|
|
||||||
else:
|
|
||||||
return f"{self.owner}: [{SexChoicesWithAll(self.sex).label}]"
|
|
||||||
|
|
||||||
|
|
||||||
class Rule(models.Model):
|
class Rule(models.Model):
|
||||||
"""
|
"""
|
||||||
Class to store rules
|
Class to store rules
|
||||||
@ -626,8 +415,6 @@ class Rule(models.Model):
|
|||||||
language = models.ForeignKey(Language, on_delete=models.PROTECT)
|
language = models.ForeignKey(Language, on_delete=models.PROTECT)
|
||||||
# Rule identifier allows to translate rules with the same identifier
|
# Rule identifier allows to translate rules with the same identifier
|
||||||
rule_identifier = models.CharField(max_length=24)
|
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):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@ -648,10 +435,9 @@ class Report(models.Model):
|
|||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text=_('ID dieses reports'),
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text=_('ID dieses reports'),
|
||||||
verbose_name=_('ID'))
|
verbose_name=_('ID'))
|
||||||
status = models.CharField(max_length=30, choices=STATES)
|
status = models.CharField(max_length=30, choices=STATES)
|
||||||
reported_broken_rules = models.ManyToManyField(Rule, verbose_name=_("Regeln gegen die verstoßen wurde"))
|
reported_broken_rules = models.ManyToManyField(Rule)
|
||||||
user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information"))
|
user_comment = models.TextField(blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"[{self.status}]: {self.user_comment:.20}"
|
return f"[{self.status}]: {self.user_comment:.20}"
|
||||||
@ -698,22 +484,20 @@ class ModerationAction(models.Model):
|
|||||||
}
|
}
|
||||||
action = models.CharField(max_length=30, choices=ACTIONS.items())
|
action = models.CharField(max_length=30, choices=ACTIONS.items())
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
public_comment = models.TextField(blank=True)
|
public_comment = models.TextField(blank=True)
|
||||||
# Only visible to moderator
|
# Only visible to moderator
|
||||||
private_comment = models.TextField(blank=True)
|
private_comment = models.TextField(blank=True)
|
||||||
report = models.ForeignKey(Report, on_delete=models.CASCADE)
|
report = models.ForeignKey(Report, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# TODO: Needs field for moderator that performed the action
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"[{self.action}]: {self.public_comment}"
|
return f"[{self.action}]: {self.public_comment}"
|
||||||
|
|
||||||
|
|
||||||
class TextTypeChoices(models.TextChoices):
|
"""
|
||||||
DEDICATED = "dedicated", _("Fest zugeordnet")
|
Membership
|
||||||
MALE = "M", _("Männlich")
|
"""
|
||||||
MALE_NEUTERED = "M_N", _("Männlich, kastriert")
|
|
||||||
FEMALE_NEUTERED = "F_N", _("Weiblich, kastriert")
|
|
||||||
INTER = "I", _("Intergeschlechtlich")
|
|
||||||
|
|
||||||
|
|
||||||
class Text(models.Model):
|
class Text(models.Model):
|
||||||
@ -732,17 +516,6 @@ class Text(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title} ({self.language})"
|
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):
|
class Announcement(Text):
|
||||||
"""
|
"""
|
||||||
@ -750,8 +523,7 @@ class Announcement(Text):
|
|||||||
"""
|
"""
|
||||||
logged_in_only = models.BooleanField(default=False)
|
logged_in_only = models.BooleanField(default=False)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunk")
|
||||||
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
|
|
||||||
publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
|
publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
|
||||||
IMPORTANT = "important"
|
IMPORTANT = "important"
|
||||||
WARNING = "warning"
|
WARNING = "warning"
|
||||||
@ -799,7 +571,6 @@ class Comment(models.Model):
|
|||||||
"""
|
"""
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
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'))
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||||
text = models.TextField(verbose_name="Inhalt")
|
text = models.TextField(verbose_name="Inhalt")
|
||||||
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
|
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
|
||||||
@ -817,7 +588,6 @@ class Comment(models.Model):
|
|||||||
|
|
||||||
class BaseNotification(models.Model):
|
class BaseNotification(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
text = models.TextField(verbose_name="Inhalt")
|
text = models.TextField(verbose_name="Inhalt")
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
@ -835,58 +605,14 @@ class CommentNotification(BaseNotification):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
|
print(f"URL: self.comment.get_absolute_url()")
|
||||||
return self.comment.get_absolute_url
|
return self.comment.get_absolute_url
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeNotification(BaseNotification):
|
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
return self.adoption_notice.get_absolute_url
|
|
||||||
|
|
||||||
|
|
||||||
class Subscriptions(models.Model):
|
class Subscriptions(models.Model):
|
||||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
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}"
|
|
||||||
|
|
||||||
|
|
||||||
class SpeciesSpecificURL(models.Model):
|
|
||||||
"""
|
|
||||||
Model that allows to specify a URL for a rescue organization where a certain species can be found
|
|
||||||
"""
|
|
||||||
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
|
||||||
rescues_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
|
||||||
verbose_name=_("Tierschutzorganisation"))
|
|
||||||
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
|
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
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,7 +1,3 @@
|
|||||||
/***************/
|
|
||||||
/* MAIN COLORS */
|
|
||||||
/***************/
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-light-one: #5daa68;
|
--primary-light-one: #5daa68;
|
||||||
--primary-light-two: #4a9455;
|
--primary-light-two: #4a9455;
|
||||||
@ -23,9 +19,6 @@
|
|||||||
--shadow-three: var(--primary-dark-one);
|
--shadow-three: var(--primary-dark-one);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**************************/
|
|
||||||
/* TAG SETTINGS (GENERAL) */
|
|
||||||
/**************************/
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -37,49 +30,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
a {
|
.content-box {
|
||||||
color: inherit;
|
margin: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
a {
|
||||||
.responsive thead {
|
text-decoration: none;
|
||||||
visibility: hidden;
|
color: inherit;
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive tr {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive td {
|
|
||||||
border: 1px solid;
|
|
||||||
border-bottom: none;
|
|
||||||
display: block;
|
|
||||||
font-size: .8em;
|
|
||||||
text-align: right;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive td::before {
|
|
||||||
content: attr(data-label);
|
|
||||||
float: left;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive td:last-child {
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@ -101,7 +62,7 @@ td {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead td {
|
th {
|
||||||
border: 3px solid black;
|
border: 3px solid black;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -118,72 +79,67 @@ h1, h2 {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
.header {
|
||||||
border-radius: 10px;
|
overflow: hidden;
|
||||||
|
background-color: var(--background-two);
|
||||||
|
border-bottom-left-radius: 15px;
|
||||||
|
border-bottom-right-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-one);
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header a, .header form {
|
||||||
|
float: left;
|
||||||
|
padding: 5px 12px 5px 12px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header a:hover {
|
||||||
|
background-color: var(--highlight-one);
|
||||||
|
color: var(--highlight-one-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header a.active {
|
||||||
|
background-color: dodgerblue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, .button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 5px;
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--secondary-light-one);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right select.option {
|
||||||
|
color: #000;
|
||||||
|
background-color: var(--highlight-one);
|
||||||
|
border: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
/**************/
|
.header-right {
|
||||||
/* CONTAINERS */
|
float: right;
|
||||||
/**************/
|
|
||||||
|
|
||||||
.container-cards {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
border-radius: 0px 0px 15px 15px;
|
||||||
width: 100%;
|
background-color: var(--highlight-two);
|
||||||
}
|
color: var(--highlight-one-text);
|
||||||
|
padding: 5px 5px 0px 5px;
|
||||||
.container-cards h1,
|
height: 67px;
|
||||||
.container-cards h2 {
|
|
||||||
width: 100%; /* Make sure heading fills complete line */
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
flex: 1 25%;
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
background: var(--background-three);
|
|
||||||
color: var(--text-two);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-edit-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced > * {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************/
|
|
||||||
/* 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 {
|
.profile-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
border-radius: 0px 0px 8px 8px;
|
||||||
|
background-color: var(--highlight-two);
|
||||||
color: var(--highlight-one-text);
|
color: var(--highlight-one-text);
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.btn2 {
|
.btn2 {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -200,37 +156,22 @@ textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-comment-form {
|
@media screen and (max-width: 500px) {
|
||||||
width: 80%;
|
.header a {
|
||||||
color: var(--text-one);
|
float: none;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
b {
|
.header-right {
|
||||||
text-shadow: 2px 2px var(--shadow-one);
|
float: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*************/
|
.logo img {
|
||||||
/* Modifiers */
|
height: 40px;
|
||||||
/*************/
|
|
||||||
|
|
||||||
/* 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 {
|
.btn {
|
||||||
background-color: var(--primary-light-one);
|
background-color: var(--primary-light-one);
|
||||||
@ -239,14 +180,9 @@ select, .button {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.btn, a.btn2, a.nav-link {
|
.btn2 {
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn2, .btn3 {
|
|
||||||
background-color: var(--secondary-light-one);
|
background-color: var(--secondary-light-one);
|
||||||
color: var(--primary-dark-one);
|
color: var(--primary-dark-one);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@ -255,313 +191,6 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn3 {
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
left: 0.2rem;
|
|
||||||
bottom: 0.075rem;
|
|
||||||
background-color: var(--primary-light-one);
|
|
||||||
color: var(--secondary-light-one);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch {
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-switch {
|
|
||||||
display: inline-block;
|
|
||||||
background: #ccc;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 58px;
|
|
||||||
height: 32px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
transition: background 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-switch:before, .toggle-switch:after {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-switch:before {
|
|
||||||
display: block;
|
|
||||||
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
left: 4px;
|
|
||||||
transition: left 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle:hover .toggle-switch:before {
|
|
||||||
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
|
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checked + .toggle-switch {
|
|
||||||
background: #56c080;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checked + .toggle-switch:before {
|
|
||||||
left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-checkbox {
|
|
||||||
position: absolute;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-label {
|
|
||||||
margin-left: 5px;
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Refactor tooltip based on https://luigicavalieri.com/blog/css-tooltip-appearing-from-any-direction/ to allow different directions */
|
|
||||||
.tooltip {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:hover .tooltiptext {
|
|
||||||
display: flex;
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .tooltiptext {
|
|
||||||
border-radius: 4px;
|
|
||||||
bottom: calc(100% + 0.6em + 2px);
|
|
||||||
box-shadow: 0px 2px 4px #07172258;
|
|
||||||
background-color: var(--primary-dark-one);
|
|
||||||
color: var(--secondary-light-one);
|
|
||||||
font-size: 0.68rem;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1.35em;
|
|
||||||
padding: 0.5em 0.7em;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
width: 7rem;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s ease-in;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .tooltiptext::before {
|
|
||||||
border-width: 0.6em 0.8em 0;
|
|
||||||
border-color: transparent;
|
|
||||||
border-top-color: var(--primary-dark-one);
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
border-style: solid;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Makes the tooltip fly from above */
|
|
||||||
.tooltip.top .tooltiptext {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.top:hover .tooltiptext {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make adjustments for bottom */
|
|
||||||
.tooltip.bottom .tooltiptext {
|
|
||||||
top: calc(100% + 0.6em + 2px);
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottom:hover .tooltiptext {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip.bottom .tooltiptext::before {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
/* 100% of the height of .tooltip */
|
|
||||||
bottom: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:not(.top) .tooltiptext {
|
|
||||||
bottom: auto;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:not(.top) .tooltiptext::before {
|
|
||||||
top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********************/
|
|
||||||
/* UNIQUE COMPONENTS */
|
|
||||||
/*********************/
|
|
||||||
|
|
||||||
.content-box {
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--background-two);
|
|
||||||
border-bottom-left-radius: 15px;
|
|
||||||
border-bottom-right-radius: 15px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
|
||||||
color: #FFF;
|
|
||||||
height: 50px;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#main-menu {
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu > li {
|
|
||||||
margin: 0 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button-container {
|
|
||||||
display: none;
|
|
||||||
height: 100%;
|
|
||||||
width: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: #4ab457;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button,
|
|
||||||
.menu-button::before,
|
|
||||||
.menu-button::after {
|
|
||||||
display: block;
|
|
||||||
background-color: #fff;
|
|
||||||
position: absolute;
|
|
||||||
height: 4px;
|
|
||||||
width: 30px;
|
|
||||||
transition: transform 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button::before {
|
|
||||||
content: '';
|
|
||||||
margin-top: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button::after {
|
|
||||||
content: '';
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-toggle:checked + .menu-button-container .menu-button::before {
|
|
||||||
margin-top: 0px;
|
|
||||||
transform: rotate(405deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-toggle:checked + .menu-button-container .menu-button {
|
|
||||||
background: rgba(255, 255, 255, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-toggle:checked + .menu-button-container .menu-button::after {
|
|
||||||
margin-top: 0px;
|
|
||||||
transform: rotate(-405deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.menu-button-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
margin-top: 50px;
|
|
||||||
left: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-toggle ~ nav .menu li {
|
|
||||||
height: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-toggle:checked ~ nav .menu li {
|
|
||||||
height: 3em;
|
|
||||||
padding: 1em;
|
|
||||||
transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu > li {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5em 0;
|
|
||||||
width: 100%;
|
|
||||||
color: white;
|
|
||||||
background-color: var(--background-two);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu > li:not(:last-child) {
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header-sign-out, #header-change-language {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-button, .link-button a:link, .link-button a:visited {
|
.form-button, .link-button a:link, .link-button a:visited {
|
||||||
background-color: #4ba3cd;
|
background-color: #4ba3cd;
|
||||||
color: white;
|
color: white;
|
||||||
@ -574,6 +203,14 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-edit-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-button:hover, .link-button a:hover, .link-button a:active {
|
.form-button:hover, .link-button a:hover, .link-button a:active {
|
||||||
background-color: #4090b6;
|
background-color: #4090b6;
|
||||||
@ -689,6 +326,18 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.tag {
|
||||||
border: black 1px solid;
|
border: black 1px solid;
|
||||||
@ -706,6 +355,12 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.container-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.photos {
|
.photos {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -721,6 +376,14 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
border-radius: 10%;
|
border-radius: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
flex: 1 25%;
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 5px;
|
||||||
|
background: var(--background-three);
|
||||||
|
color: var(--text-two);
|
||||||
|
}
|
||||||
|
|
||||||
.card h1 {
|
.card h1 {
|
||||||
color: var(--text-three);
|
color: var(--text-three);
|
||||||
@ -740,22 +403,12 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
|
|
||||||
.header-card-adoption-notice {
|
.header-card-adoption-notice {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-subscription-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-adoption-notice-info {
|
.table-adoption-notice-info {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
@ -781,6 +434,7 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
.btn-notification {
|
.btn-notification {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make the badge float in the top right corner of the button */
|
/* Make the badge float in the top right corner of the button */
|
||||||
@ -800,30 +454,15 @@ a.btn, a.btn2, a.nav-link {
|
|||||||
.adoption-card-report-link, .notification-card-mark-read {
|
.adoption-card-report-link, .notification-card-mark-read {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adoption-card-report-link {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.notification-card-mark-read {
|
.notification-card-mark-read {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-container {
|
.heading-card-adoption-notice {
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-container > * {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2.heading-card-adoption-notice {
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
word-wrap: anywhere;
|
word-wrap: anywhere;
|
||||||
width: 80%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
@ -838,15 +477,17 @@ h2.heading-card-adoption-notice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-adoption-notice-header h1 {
|
.detail-adoption-notice-header h1 {
|
||||||
|
width: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-adoption-notice-header a {
|
.detail-adoption-notice-header a {
|
||||||
|
display: inline-block;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 920px) {
|
@media (max-width: 920px) {
|
||||||
.detail-adoption-notice-header .inline-container {
|
.detail-adoption-notice-header h1 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -864,7 +505,7 @@ h2.heading-card-adoption-notice {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment, .notification, .search-subscription {
|
.comment, .notification {
|
||||||
flex: 1 100%;
|
flex: 1 100%;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -873,6 +514,20 @@ h2.heading-card-adoption-notice {
|
|||||||
color: var(--text-two);
|
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 {
|
.form-comments {
|
||||||
.btn {
|
.btn {
|
||||||
@ -880,16 +535,7 @@ h2.heading-card-adoption-notice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.announcement-header {
|
.announcement {
|
||||||
font-size: 1.2rem;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: var(--text-two);
|
|
||||||
text-shadow: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.announcement {
|
|
||||||
flex: 1 100%;
|
flex: 1 100%;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -897,60 +543,15 @@ div.announcement {
|
|||||||
background: var(--background-three);
|
background: var(--background-three);
|
||||||
color: var(--text-two);
|
color: var(--text-two);
|
||||||
|
|
||||||
}
|
h1 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0px;
|
||||||
.form-search {
|
padding: 0px;
|
||||||
select, input {
|
color: var(--text-two);
|
||||||
background-color: var(--primary-light-one);
|
text-shadow: none;
|
||||||
color: var(--text-one);
|
|
||||||
border-radius: 3px;
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.half {
|
|
||||||
width: 49%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#results {
|
|
||||||
margin-top: 10px;
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item {
|
|
||||||
padding: 8px;
|
|
||||||
margin: 4px 0;
|
|
||||||
background-color: #ddd1a5;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item:hover {
|
|
||||||
background-color: #ede1b5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-adoption {
|
|
||||||
background-color: #4a9455;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inactive-adoption {
|
|
||||||
background-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************/
|
|
||||||
/* GENERAL HIGHLIGHTING */
|
|
||||||
/************************/
|
|
||||||
|
|
||||||
.important {
|
.important {
|
||||||
border: #e01137 4px solid;
|
border: #e01137 4px solid;
|
||||||
}
|
}
|
||||||
@ -963,19 +564,16 @@ div.announcement {
|
|||||||
border: rgba(17, 58, 224, 0.51) 4px solid;
|
border: rgba(17, 58, 224, 0.51) 4px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.form-search {
|
||||||
color: #370707;
|
select, input {
|
||||||
font-weight: bold;
|
background-color: var(--primary-light-one);
|
||||||
}
|
color: var(--text-one);
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.error::before {
|
|
||||||
content: "⚠️";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******/
|
|
||||||
/* MAP */
|
|
||||||
/*******/
|
|
||||||
|
|
||||||
.marker {
|
.marker {
|
||||||
background-image: url('../img/logo_transparent.png');
|
background-image: url('../img/logo_transparent.png');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@ -984,11 +582,6 @@ div.announcement {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animal-shelter-marker {
|
|
||||||
background-image: url('../img/animal_shelter.png');
|
|
||||||
!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maplibregl-popup {
|
.maplibregl-popup {
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
@ -1,21 +0,0 @@
|
|||||||
function ifdef(variable, prefix = "", suffix = "") {
|
|
||||||
if (variable !== undefined) {
|
|
||||||
return prefix + variable + suffix;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function geojson_to_summary(location) {
|
|
||||||
if (ifdef(location.properties.name) !== "") {
|
|
||||||
return location.properties.name + ifdef(location.properties.city, " (", ")");
|
|
||||||
} else {
|
|
||||||
return ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ","")) + ifdef(location.properties.city, ", ", "") + ifdef(location.properties.countrycode, ", ", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function geojson_to_searchable_string(location) {
|
|
||||||
return ifdef(location.properties.name, "", ", ") + ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ",", ")) + ifdef(location.properties.city, "", ", ") + ifdef(location.properties.country, "", "")
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
@ -1,59 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
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
|
|
||||||
from .tools.notifications import notify_moderators_of_AN_to_be_checked
|
|
||||||
from .tools.search import notify_search_subscribers
|
|
||||||
|
|
||||||
|
|
||||||
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.post_an_save")
|
|
||||||
def post_adoption_notice_save(pk):
|
|
||||||
instance = AdoptionNotice.objects.get(pk=pk)
|
|
||||||
Location.add_location_to_object(instance)
|
|
||||||
set_timestamp("add_adoption_notice_location")
|
|
||||||
logging.info(f"Location was added to Adoption notice {pk}")
|
|
||||||
|
|
||||||
notify_search_subscribers(instance, only_if_active=True)
|
|
||||||
notify_moderators_of_AN_to_be_checked(instance)
|
|
||||||
|
|
||||||
@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,54 +2,22 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
|
|
||||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if about_us %}
|
<h1>{% translate "Regeln" %}</h1>
|
||||||
<div class="card">
|
|
||||||
<h1>{{ about_us.title }}</h1>
|
|
||||||
<p>
|
|
||||||
{{ about_us.content | render_markdown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>{% translate "Regeln" %}</h2>
|
|
||||||
{% include "fellchensammlung/lists/list-rules.html" %}
|
{% include "fellchensammlung/lists/list-rules.html" %}
|
||||||
|
|
||||||
{% if faq %}
|
|
||||||
<div class="card">
|
|
||||||
<h2>{{ faq.title }}</h2>
|
|
||||||
<p>
|
|
||||||
{{ faq.content | render_markdown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if privacy_statement %}
|
{% if privacy_statement %}
|
||||||
<div class="card">
|
<h1>{{ privacy_statement.title }}</h1>
|
||||||
<h2>{{ privacy_statement.title }}</h2>
|
{{ privacy_statement.content | render_markdown }}
|
||||||
<p>
|
|
||||||
{{ privacy_statement.content | render_markdown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if terms_of_service %}
|
{% if terms_of_service %}
|
||||||
<div class="card">
|
<h1>{{ terms_of_service.title }}</h1>
|
||||||
<h2>{{ terms_of_service.title }}</h2>
|
{{ terms_of_service.content | render_markdown }}
|
||||||
<p>
|
|
||||||
{{ terms_of_service.content | render_markdown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if imprint %}
|
{% if imprint %}
|
||||||
<div class="card">
|
<h1>{{ imprint.title }}</h1>
|
||||||
<h2>{{ imprint.title }}</h2>
|
{{ imprint.content | render_markdown }}
|
||||||
<p>
|
|
||||||
{{ imprint.content | render_markdown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,13 +0,0 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}<title>{% translate "Tierschutzorganisationen" %}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container-cards">
|
|
||||||
<div class="card">
|
|
||||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
|
||||||
{% endblock %}
|
|
@ -1,12 +1,10 @@
|
|||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
{% load i18n %}
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% block title %}{% endblock %}
|
{% block title %}<title>Notfellchen</title>{% endblock %}
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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 -->
|
<!-- Add additional CSS in static file -->
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">
|
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">
|
||||||
@ -14,8 +12,6 @@
|
|||||||
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
|
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
|
||||||
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
|
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
|
||||||
|
|
||||||
<script src="{% static 'fellchensammlung/js/custom.js' %}"></script>
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'fellchensammlung/favicon/apple-touch-icon.png' %}">
|
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'fellchensammlung/favicon/apple-touch-icon.png' %}">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'fellchensammlung/favicon/favicon-32x32.png' %}">
|
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'fellchensammlung/favicon/favicon-32x32.png' %}">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'fellchensammlung/favicon/favicon-16x16.png' %}">
|
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'fellchensammlung/favicon/favicon-16x16.png' %}">
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
|
||||||
{% load custom_tags %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}<title>{{ org.name }}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container-cards">
|
|
||||||
<div class="card half">
|
|
||||||
<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>
|
|
||||||
<table class="responsive">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{% if org.website %}
|
|
||||||
<td>{% translate "Website" %}</td>
|
|
||||||
{% endif %}
|
|
||||||
{% if org.phone_number %}
|
|
||||||
<td>{% translate "Telefonnummer" %}</td>
|
|
||||||
{% endif %}
|
|
||||||
{% if org.email %}
|
|
||||||
<td>{% translate "E-Mail" %}</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tr>
|
|
||||||
{% if org.website %}
|
|
||||||
<td data-label="{% trans 'Website' %} ">
|
|
||||||
{{ org.website }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
{% if org.phone_number %}
|
|
||||||
<td data-label="{% trans 'Telefonnummer' %}">
|
|
||||||
{{ org.phone_number }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if org.email %}
|
|
||||||
<td data-label="{% trans 'E-Mail' %}">
|
|
||||||
{{ org.email }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card half">
|
|
||||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
|
||||||
</div>
|
|
||||||
</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 %}
|
|
@ -1,83 +1,24 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
{% extends "fellchensammlung/base_generic.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ user.get_full_name }}</h1>
|
<h1>{{ user.get_full_name }}</h1>
|
||||||
<div class="spaced">
|
|
||||||
|
|
||||||
<div class="container-cards">
|
<p><strong>{% translate "Username" %}:</strong> {{ user.username }}</p>
|
||||||
<h2>{% trans 'Daten' %}</h2>
|
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
|
||||||
<div class="card">
|
|
||||||
<p><strong>{% translate "Username" %}:</strong> {{ user.username }}</p>
|
|
||||||
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{% if user.preferred_language %}
|
||||||
|
<p><strong>{% translate "Sprache" %}:</strong> {{ user.preferred_language }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="container-cards">
|
{% if user.id is request.user.id %}
|
||||||
<h2>{% trans 'Profil verwalten' %}</h2>
|
|
||||||
<div class="container-comment-form">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if user.id is request.user.id %}
|
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
||||||
<div class="detail-animal-header"><h2>{% trans 'Einstellungen' %}</h2></div>
|
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||||
<div class="container-cards">
|
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
||||||
<form class="card" action="" method="POST">
|
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||||
{% csrf_token %}
|
|
||||||
{% if user.email_notifications %}
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
|
|
||||||
<div class="toggle-switch round "></div>
|
|
||||||
<span class="slider-label">
|
|
||||||
{% translate 'E-Mail Benachrichtigungen' %}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
{% else %}
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
|
|
||||||
<div class="toggle-switch round"></div>
|
|
||||||
<span class="slider-label">
|
|
||||||
{% translate 'E-Mail Benachrichtigungen' %}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
{% endif %}
|
||||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
|
||||||
|
|
||||||
<h2>{% translate 'Abonnierte Suchen' %}</h2>
|
|
||||||
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
|
|
||||||
|
|
||||||
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
|
||||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -2,43 +2,25 @@
|
|||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="detail-adoption-notice-header">
|
<div class="detail-adoption-notice-header">
|
||||||
<div class="inline-container">
|
<h1 class="detail-adoption-notice-header">{{ adoption_notice.name }}
|
||||||
<h1>{{ adoption_notice.name }}</h1>
|
|
||||||
{% if not is_subscribed %}
|
{% if not is_subscribed %}
|
||||||
<div class="tooltip bottom">
|
<form class="notification-card-mark-read" method="post">
|
||||||
<form class="notification-card-mark-read" method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input type="hidden" name="action" value="subscribe">
|
||||||
<input type="hidden" name="action" value="subscribe">
|
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||||
<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>
|
||||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
|
</form>
|
||||||
</form>
|
|
||||||
<span class="tooltiptext">
|
|
||||||
{% translate 'Abonniere diese Vermittlung um bei Kommentaren oder Statusänderungen benachrichtigt zu werden' %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tooltip bottom">
|
<form class="notification-card-mark-read" method="post">
|
||||||
<form class="notification-card-mark-read" method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input type="hidden" name="action" value="unsubscribe">
|
||||||
<input type="hidden" name="action" value="unsubscribe">
|
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||||
<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>
|
||||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
|
</form>
|
||||||
</form>
|
|
||||||
<span class="tooltiptext">
|
|
||||||
{% translate 'Deabonnieren. Du bekommst keine Benachrichtigungen zu dieser Vermittlung mehr' %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if adoption_notice.is_active %}
|
</h1>
|
||||||
<span id="submit" class="label active-adoption" style=>{% trans 'Aktive Vermittlung' %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span id="submit" class="label inactive-adoption" style=>{% trans 'Vermittlung inaktiv' %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if has_edit_permission %}
|
{% if has_edit_permission %}
|
||||||
<a class="btn2"
|
<a class="btn2"
|
||||||
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">{% translate 'Foto hinzufügen' %}</a>
|
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">{% translate 'Foto hinzufügen' %}</a>
|
||||||
@ -47,61 +29,29 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-adoption-notice-info">
|
<div class="table-adoption-notice-info">
|
||||||
<table class="responsive">
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% translate "Ort" %}</td>
|
<th>{% translate "Ort" %}</th>
|
||||||
{% if adoption_notice.organization %}
|
<th>{% translate "Suchen seit" %}</th>
|
||||||
<td>{% translate "Organisation" %}</td>
|
<th>{% translate "Zuletzt aktualisiert" %}</th>
|
||||||
{% endif %}
|
<th>{% translate "Weitere Informationen" %}</th>
|
||||||
<td>{% translate "Suchen seit" %}</td>
|
|
||||||
<td>{% translate "Zuletzt aktualisiert" %}</td>
|
|
||||||
<td>{% translate "Weitere Informationen" %}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td data-label="{% trans 'Ort' %} ">
|
<td>
|
||||||
{% if adoption_notice.location %}
|
{% if adoption_notice.location %}
|
||||||
{{ adoption_notice.location }}
|
{{ adoption_notice.location }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ adoption_notice.location_string }}
|
{{ adoption_notice.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if adoption_notice.organization %}
|
|
||||||
<td data-label="{% trans 'Organisation' %}">
|
|
||||||
<div>
|
|
||||||
<a href="{{ adoption_notice.organization.get_absolute_url }}">{{ adoption_notice.organization }}</a>
|
|
||||||
{% if adoption_notice.organization.trusted %}
|
|
||||||
<div class="tooltip top">
|
|
||||||
<div class="checkmark"><i class="fa-solid fa-check"></i></div>
|
|
||||||
<span class="tooltiptext">
|
|
||||||
{% translate 'Diese Organisation kennt sich mit Ratten aus und achtet auf gute Abgabebedingungen' %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
{% else %}
|
||||||
|
<td>-</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td data-label="{% trans 'Suchen seit' %}">{{ adoption_notice.searching_since }}</td>
|
|
||||||
<td data-label="{% trans 'Zuletzt aktualisiert' %}">
|
|
||||||
{{ adoption_notice.last_checked_hr }}
|
|
||||||
</td>
|
|
||||||
<td data-label="{% trans 'Weitere Informationen' %}">
|
|
||||||
{% if adoption_notice.further_information %}
|
|
||||||
<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>
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}<title>{{ animal.name }}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "fellchensammlung/details/detail-animal-partial.html" %}
|
{% include "fellchensammlung/details/detail-animal-partial.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
{% 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 %}
|
|
@ -1,15 +0,0 @@
|
|||||||
{% 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,6 +8,9 @@
|
|||||||
<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE_CURRENT %} selected{% endif %}>
|
<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE_CURRENT %} selected{% endif %}>
|
||||||
{{ language.0|language_name_local }}
|
{{ language.0|language_name_local }}
|
||||||
</option>
|
</option>
|
||||||
|
<!--<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
|
{{ language.0|language_name_local }} ({{ language.0 }})
|
||||||
|
</option>-->
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<!--<input type="submit" value={% translate "change" %}>-->
|
<!--<input type="submit" value={% translate "change" %}>-->
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
Lade hier ein Foto hoch - wähle den Titel wie du willst und mach bitte eine Bildbeschreibung,
|
Lade hier ein Foto hoch - wähle den Titel wie du willst und mach bitte eine Bildbeschreibung,
|
||||||
damit die Fotos auch für blinde und sehbehinderte Personen zugänglich sind.
|
damit die Fotos auch für blinde und sehbehinderte Personen zugänglich sind.
|
||||||
{% endblocktranslate %}
|
{% endblocktranslate %}
|
||||||
<p><a class="btn2" href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a></p>
|
<p><a class="btn" href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a></p>
|
||||||
</p>
|
</p>
|
||||||
<div class="container-form">
|
<div class="container-form">
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
<form method = "post" enctype="multipart/form-data">
|
<form method = "post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p }}
|
{{ form.as_p }}
|
||||||
<button class="btn2" type="submit">{% translate "Melden" %}</button>
|
<button class="button-report" type="submit">{% translate "Melden" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -2,8 +2,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% translate "Vermitteln" %}</h1>
|
<h1>{% translate "Vermitteln" %}</h1>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,57 +1,41 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<section class="header">
|
<div class="header">
|
||||||
<div>
|
<a href="{% url "index" %}" class="logo"><img src={% static 'fellchensammlung/img/logo_transparent.png' %}></a>
|
||||||
<a href="{% url "index" %}" class="logo"><img src={% static 'fellchensammlung/img/logo_transparent.png' %}></a>
|
<nav id="nav" class="nav justify-content-center">
|
||||||
</div>
|
<a class="nav-link " href="{% url "search" %}"><i class="fas fa-search"></i> {% translate 'Suchen' %}</a>
|
||||||
|
<a class="nav-link " href="{% url "add-adoption" %}"><i
|
||||||
|
class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}</a>
|
||||||
|
<a class="nav-link " href="{% url "about" %}"><i class="fas fa-info"></i> {% translate 'Über uns' %}</a>
|
||||||
|
<a class="nav-link " href="{% url "rss" %}"><i class="fa-solid fa-rss"></i> {% translate 'RSS' %}</a>
|
||||||
|
|
||||||
<div class="profile-card">
|
</nav>
|
||||||
<div id="header-change-language">
|
<div class="header-right">
|
||||||
|
<div class="profile-card">
|
||||||
{% include "fellchensammlung/forms/change_language.html" %}
|
{% include "fellchensammlung/forms/change_language.html" %}
|
||||||
</div>
|
{% if user.is_authenticated %}
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<div class="btn2 button_darken btn-notification">
|
|
||||||
<a href="{{ user.get_notifications_url }}">
|
|
||||||
<i class="fa fa-bell" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
{% if user.get_num_unread_notifications > 0 %}
|
|
||||||
<span class="button__badge">{{ user.get_num_unread_notifications }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a class="btn2" href="{% url 'user-me' %}"><i aria-hidden="true" class="fas fa-user"></i></a>
|
<div class="btn2 button_darken btn-notification">
|
||||||
<form class="btn2 button_darken" id="header-sign-out" action="{% url 'logout' %}" method="post">
|
<a href="{{ user.get_notifications_url }}">
|
||||||
{% csrf_token %}
|
<i class="fa fa-bell" aria-hidden="true"></i>
|
||||||
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
|
|
||||||
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
|
|
||||||
{% endif %}
|
|
||||||
<input id="menu-toggle" type="checkbox"/>
|
|
||||||
<label class='menu-button-container' for="menu-toggle">
|
|
||||||
<div class='menu-button'></div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<nav id="main-menu">
|
|
||||||
<ul class="menu">
|
|
||||||
<li>
|
|
||||||
<a class="nav-link " href="{% url "search" %}">
|
|
||||||
<i class="fas fa-search"></i> {% translate 'Suchen' %}
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% if user.get_num_unread_notifications > 0 %}
|
||||||
<li><a class="nav-link " href="{% url "add-adoption" %}"><i
|
<span class="button__badge">{{ user.get_num_unread_notifications }}</span>
|
||||||
class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}</a></li>
|
{% endif %}
|
||||||
<li><a class="nav-link " href="{% url "about" %}"><i
|
|
||||||
class="fas fa-info"></i> {% translate 'Über uns' %}
|
</div>
|
||||||
</a>
|
|
||||||
</li>
|
<a class="btn2" href="{{ user.get_absolute_url }}"><i aria-hidden="true" class="fas fa-user"></i></a>
|
||||||
<li><a class="nav-link " href="{% url "rss" %}"><i
|
<form class="btn2 button_darken" action="{% url 'logout' %}" method="post">
|
||||||
class="fa-solid fa-rss"></i> {% translate 'RSS' %}
|
{% csrf_token %}
|
||||||
</a>
|
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>
|
||||||
</li>
|
</form>
|
||||||
</ul>
|
{% else %}
|
||||||
</nav>
|
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
|
||||||
|
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
@ -2,8 +2,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
|
|
||||||
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% for announcement in announcements %}
|
{% for announcement in announcements %}
|
||||||
{% include "fellchensammlung/partials/partial-announcement.html" %}
|
{% include "fellchensammlung/partials/partial-announcement.html" %}
|
||||||
|
@ -1,51 +1,28 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
{% extends "fellchensammlung/base_generic.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}<title>{% translate "Instanz-Check" %}</title> {% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>{% translate "Instanz-Check" %}</h1>
|
<h1>{% translate "Instanz-Check" %}</h1>
|
||||||
{% if missing_texts|length > 0 %}
|
{% if missing_texts|length > 0 %}
|
||||||
<h2>{% trans "Fehlende Texte" %}</h2>
|
<h2>{% trans "Fehlende Texte" %}</h2>
|
||||||
<p>
|
<p>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
|
||||||
<th>{% translate "Text Code" %}</th>
|
|
||||||
<th>{% translate "Sprache" %}</th>
|
|
||||||
</tr>
|
|
||||||
{% for missing_text in missing_texts %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ missing_text.0 }}</td>
|
<th>{% translate "Text Code" %}</th>
|
||||||
<td>{{ missing_text.1 }}</td>
|
<th>{% translate "Sprache" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% for missing_text in missing_texts %}
|
||||||
</table>
|
<tr>
|
||||||
|
<td>{{ missing_text.0 }}</td>
|
||||||
|
<td>{{ missing_text.1 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate "Texte scheinen vollständig" %}</p>
|
<p>{% translate "Texte scheinen vollständig" %}</p>
|
||||||
{% endif %}
|
{% 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>
|
<h2>{% translate "Nicht-lokalisierte Vermittlungen" %}</h2>
|
||||||
{% if number_not_geocoded_adoption_notices > 0 %}
|
{% if number_not_geocoded_adoption_notices > 0 %}
|
||||||
<details>
|
<details>
|
||||||
@ -78,22 +55,6 @@
|
|||||||
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
|
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
|
||||||
{% endif %}
|
{% 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">
|
<form class="notification-card-mark-read" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="clean_locations">
|
<input type="hidden" name="action" value="clean_locations">
|
||||||
@ -101,21 +62,5 @@
|
|||||||
<i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %}
|
<i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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 "Deaktiviere ungeprüfte Vermittlungen" %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="notification-card-mark-read" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="deactivate_404">
|
|
||||||
<button class="btn" type="submit" id="submit">
|
|
||||||
<i class="fa-solid fa-broom"></i> {% translate "Deaktiviere 404 Vermittlungen" %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="container-cards">
|
<div class="container-cards">
|
||||||
{% if adoption_notices %}
|
{% if adoption_notices %}
|
||||||
{% for adoption_notice in adoption_notices %}
|
{% for adoption_notice in adoption_notices %}
|
||||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
{% include "fellchensammlung/partials/partial-adoption-notice.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
<div class="container-cards spaced">
|
|
||||||
{% if rescue_organizations %}
|
|
||||||
{% for rescue_organization in rescue_organizations %}
|
|
||||||
{% include "fellchensammlung/partials/partial-rescue-organization.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p>{% translate "Keine Tierschutzorganisationen gefunden." %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
@ -1,10 +1,5 @@
|
|||||||
{% load i18n %}
|
|
||||||
<div class="container-cards">
|
<div class="container-cards">
|
||||||
{% if notifications %}
|
{% for notification in notifications %}
|
||||||
{% for notification in notifications %}
|
{% include "fellchensammlung/partials/partial-notification.html" %}
|
||||||
{% include "fellchensammlung/partials/partial-notification.html" %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p>{% translate 'Keine ungelesenen Benachrichtigungen' %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
<div class="container-cards">
|
|
||||||
{% if search_subscriptions %}
|
|
||||||
{% for search_subscription in search_subscriptions %}
|
|
||||||
{% include "fellchensammlung/partials/partial-search-subscription.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p>{% translate 'Keine abonnierten Suchen' %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
@ -1,6 +1,5 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
{% extends "fellchensammlung/base_generic.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}<title>{% translate "Karte" %}</title> %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
{% extends "fellchensammlung/base_generic.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}<title>{% translate "Modqueue" %}</title> %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% translate "Modqueue" %}</h1>
|
<h1>{% translate "Modqueue" %}</h1>
|
||||||
|
@ -2,34 +2,31 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="header-card-adoption-notice">
|
<div>
|
||||||
<h2 class="heading-card-adoption-notice"><a
|
<div class="header-card-adoption-notice">
|
||||||
href="{{ adoption_notice.get_absolute_url }}"> {{ adoption_notice.name }}</a></h2>
|
<h1><a class="heading-card-adoption-notice"
|
||||||
<div class="tooltip bottom">
|
href="{{ adoption_notice.get_absolute_url }}"> {{ adoption_notice.name }}</a></h1>
|
||||||
<a class="adoption-card-report-link" href="{{ adoption_notice.get_report_url }}"><i
|
<a class="adoption-card-report-link" href="{{ adoption_notice.get_report_url }}"><i
|
||||||
class="fa-solid fa-flag"></i></a>
|
class="fa-solid fa-flag"></i></a>
|
||||||
<span class="tooltiptext">
|
|
||||||
{% translate 'Melde diese Vermittlung an Moderator*innen' %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p>
|
||||||
|
<b>Ort</b>
|
||||||
|
{% if adoption_notice.location %}
|
||||||
|
{{ adoption_notice.location }}
|
||||||
|
{% else %}
|
||||||
|
{{ adoption_notice.location_string }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% if adoption_notice.description_short %}
|
||||||
|
{{ adoption_notice.description_short | render_markdown }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if adoption_notice.get_photo %}
|
||||||
|
<div class="adoption-notice-img img-small">
|
||||||
|
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
|
||||||
|
alt="{{ adoption_notice.get_photo.alt_text }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
|
||||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
|
||||||
{% if adoption_notice.location %}
|
|
||||||
{{ adoption_notice.location.str_hr }}
|
|
||||||
{% else %}
|
|
||||||
{{ adoption_notice.location_string }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{% if adoption_notice.description_short %}
|
|
||||||
{{ adoption_notice.description_short | render_markdown }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
{% if adoption_notice.get_photo %}
|
|
||||||
<div class="adoption-notice-img img-small">
|
|
||||||
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
|
|
||||||
alt="{{ adoption_notice.get_photo.alt_text }}">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
@ -14,7 +14,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<b>Ort</b>
|
<b>Ort</b>
|
||||||
{% if adoption_notice.location %}
|
{% if adoption_notice.location %}
|
||||||
{{ adoption_notice.location.str_hr }}
|
{{ adoption_notice.location }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ adoption_notice.location_string }}
|
{{ adoption_notice.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="detail-animal-header">
|
<div class="detail-animal-header">
|
||||||
<h1><a href="{% url 'animal-detail' animal_id=animal.pk %}">{{ animal.name }}</a></h1>
|
<h1><a href="{% url 'animal-detail' animal_id=animal.pk %}">{{ animal.name }}</a></h1>
|
||||||
@ -20,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if animal.description %}
|
{% if animal.description %}
|
||||||
<p>{{ animal.description | render_markdown }}</p>
|
<p>{{ animal.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for photo in animal.get_photos %}
|
{% for photo in animal.get_photos %}
|
||||||
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
|
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
<div class="announcement {{ announcement.type }}">
|
<div class="announcement {{ announcement.type }}">
|
||||||
<details class="announcement" open>
|
<div class="announcement-header">
|
||||||
<summary class="announcement-header">{{ announcement.title }}</summary>
|
<h1 class="announcement">{{ announcement.title }}</h1>
|
||||||
<p>
|
</div>
|
||||||
{{ announcement.content | render_markdown }}
|
<p>
|
||||||
</p>
|
{{ announcement.content | render_markdown }}
|
||||||
</details>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load custom_tags %}
|
|
||||||
<div class="card">
|
|
||||||
<h1>
|
|
||||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
|
||||||
</h1>
|
|
||||||
<i>{% translate 'Zuletzt geprüft:' %} {{ adoption_notice.last_checked_hr }}</i>
|
|
||||||
{% if adoption_notice.further_information %}
|
|
||||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
|
||||||
{% endif %}
|
|
||||||
<div class="container-edit-buttons">
|
|
||||||
<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,7 +1,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div class="container-comments">
|
<div class="container-comments">
|
||||||
<h2>{% translate 'Kommentare' %}</h2>
|
<h2>{% translate 'Comments' %}</h2>
|
||||||
{% if adoption_notice.comments %}
|
{% if adoption_notice.comments %}
|
||||||
{% for comment in adoption_notice.comments %}
|
{% for comment in adoption_notice.comments %}
|
||||||
{% include "fellchensammlung/partials/partial-comment.html" %}
|
{% include "fellchensammlung/partials/partial-comment.html" %}
|
||||||
|
@ -6,141 +6,35 @@
|
|||||||
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
|
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
|
||||||
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
|
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
|
||||||
|
|
||||||
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
|
|
||||||
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- add container for the map -->
|
<!-- add container for the map -->
|
||||||
<div id="map" style="width:100%;aspect-ratio:16/9"></div>
|
<div id="map" style="width:100%;aspect-ratio:16/9"></div>
|
||||||
|
|
||||||
<!-- start map -->
|
<!-- start map -->
|
||||||
<script>
|
<script>
|
||||||
{% if zoom_level %}
|
|
||||||
var zoom_level = {{ zoom_level }};
|
|
||||||
{% else %}
|
|
||||||
var zoom_level = 4;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if map_center %}
|
|
||||||
var map_center = [{{ map_center.longitude | pointdecimal }}, {{ map_center.latitude | pointdecimal }}];
|
|
||||||
{% else %}
|
|
||||||
var map_center = [10.49, 50.68]; <!-- Point middle of Germany -->
|
|
||||||
zoom_level = 4; //Overwrite zoom level when no place is found
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
let map = new maplibregl.Map({
|
let map = new maplibregl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: '{% static "fellchensammlung/map/styles/colorful.json" %}',
|
style: '{% static "fellchensammlung/map/styles/colorful.json" %}',
|
||||||
center: map_center,
|
center: [10.49, 50.68],
|
||||||
zoom: zoom_level
|
zoom: 5
|
||||||
}).addControl(new maplibregl.NavigationControl());
|
}).addControl(new maplibregl.NavigationControl());
|
||||||
|
|
||||||
{% for adoption_notice in adoption_notices_map %}
|
{% for adoption_notice in adoption_notices_map %}
|
||||||
{% if adoption_notice.location %}
|
{% if adoption_notice.location %}
|
||||||
// create the popup
|
// create the popup
|
||||||
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}`);
|
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}`);
|
||||||
|
|
||||||
// create DOM element for the marker
|
// create DOM element for the marker
|
||||||
const el_{{ forloop.counter }} = document.createElement('div');
|
const el_{{ forloop.counter }} = document.createElement('div');
|
||||||
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
|
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
|
||||||
el_{{ forloop.counter }}.classList.add('marker');
|
el_{{ forloop.counter }}.classList.add('marker');
|
||||||
|
|
||||||
const location_popup_{{ forloop.counter }} = [{{ adoption_notice.location.longitude | pointdecimal }}, {{ adoption_notice.location.latitude | pointdecimal }}];
|
const location_popup_{{ forloop.counter }} = [{{ adoption_notice.location.longitude | pointdecimal }}, {{ adoption_notice.location.latitude | pointdecimal }}];
|
||||||
// create the marker
|
// create the marker
|
||||||
new maplibregl.Marker({element: el_{{ forloop.counter }}})
|
new maplibregl.Marker({element: el_{{ forloop.counter }}})
|
||||||
.setLngLat(location_popup_{{ forloop.counter }})
|
.setLngLat(location_popup_{{ forloop.counter }})
|
||||||
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
|
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for rescue_organization in rescue_organizations %}
|
|
||||||
{% if rescue_organization.location %}
|
|
||||||
// create the popup
|
|
||||||
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-rescue-organization.html" %}`);
|
|
||||||
|
|
||||||
// create DOM element for the marker
|
|
||||||
const el_{{ forloop.counter }} = document.createElement('div');
|
|
||||||
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
|
|
||||||
el_{{ forloop.counter }}.classList.add('animal-shelter-marker', 'marker');
|
|
||||||
|
|
||||||
const location_popup_{{ forloop.counter }} = [{{ rescue_organization.location.longitude | pointdecimal }}, {{ rescue_organization.location.latitude | pointdecimal }}];
|
|
||||||
// create the marker
|
|
||||||
new maplibregl.Marker({element: el_{{ forloop.counter }}})
|
|
||||||
.setLngLat(location_popup_{{ forloop.counter }})
|
|
||||||
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
|
|
||||||
.addTo(map);
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
map.on('load', async () => {
|
|
||||||
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
|
|
||||||
map.addImage('pin', image.data);
|
|
||||||
{% for map_pin in map_pins %}
|
|
||||||
map.addSource('point', {
|
|
||||||
'type': 'geojson',
|
|
||||||
'data': {
|
|
||||||
'type': 'FeatureCollection',
|
|
||||||
'features': [
|
|
||||||
{
|
|
||||||
'type': 'Feature',
|
|
||||||
'geometry': {
|
|
||||||
'type': 'Point',
|
|
||||||
'coordinates': [{{ map_pin.location.longitude | pointdecimal }}, {{ map_pin.location.latitude | pointdecimal }}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
{% endfor %}
|
|
||||||
map.addLayer({
|
|
||||||
'id': 'pints',
|
|
||||||
'type': 'symbol',
|
|
||||||
'source': 'point',
|
|
||||||
'layout': {
|
|
||||||
'icon-image': 'pin',
|
|
||||||
'icon-size': 0.1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if search_center %}
|
|
||||||
var search_center = [{{ search_center.longitude | pointdecimal }}, {{ search_center.latitude | pointdecimal }}];
|
|
||||||
map.on('load', () => {
|
|
||||||
const radius = {{ search_radius }}; // kilometer
|
|
||||||
const options = {
|
|
||||||
steps: 64,
|
|
||||||
units: 'kilometers'
|
|
||||||
};
|
|
||||||
const circle = turf.circle(search_center, radius, options);
|
|
||||||
|
|
||||||
// Add the circle as a GeoJSON source
|
|
||||||
map.addSource('location-radius', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: circle
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a fill layer with some transparency
|
|
||||||
map.addLayer({
|
|
||||||
id: 'location-radius',
|
|
||||||
type: 'fill',
|
|
||||||
source: 'location-radius',
|
|
||||||
paint: {
|
|
||||||
'fill-color': 'rgba(140,207,255,0.3)',
|
|
||||||
'fill-opacity': 0.5
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a line layer to draw the circle outline
|
|
||||||
map.addLayer({
|
|
||||||
id: 'location-radius-outline',
|
|
||||||
type: 'line',
|
|
||||||
source: 'location-radius',
|
|
||||||
paint: {
|
|
||||||
'line-color': '#0094ff',
|
|
||||||
'line-width': 3
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<p><b>{% translate "Kommentar zur Meldung" %}:</b>
|
<p><b>{% translate "Kommentar zur Meldung" %}:</b>
|
||||||
{{ report.user_comment }}
|
{{ report.user_comment }}
|
||||||
</p>
|
</p>
|
||||||
<div class="container-edit-buttons">
|
<div>
|
||||||
<form action="allow" class="">
|
<form action="allow" class="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
{% load custom_tags %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div>
|
|
||||||
<h2 class="heading-card-adoption-notice"><a
|
|
||||||
href="{{ rescue_organization.get_absolute_url }}"> {{ rescue_organization.name }}</a></h2>
|
|
||||||
<p>
|
|
||||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
|
||||||
{% if rescue_organization.location %}
|
|
||||||
{{ rescue_organization.location.str_hr }}
|
|
||||||
{% else %}
|
|
||||||
{{ rescue_organization.location_string }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{% if rescue_organization.description_short %}
|
|
||||||
{{ rescue_organization.description_short | render_markdown }}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
{% load custom_tags %}
|
|
||||||
<div class="search-subscription">
|
|
||||||
<div class="search-subscription-header">
|
|
||||||
<h3>{{ search_subscription }}</h3>
|
|
||||||
<form class="" method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="search_subscription_delete">
|
|
||||||
<input type="hidden" name="search_subscription_id" value="{{ search_subscription.pk }}">
|
|
||||||
<button class="btn3" type="submit" id="submit"><i class="fa-solid fa-close"></i></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans 'Geschlecht' %}</th>
|
|
||||||
<th>{% trans 'Suchort' %}</th>
|
|
||||||
<th>{% trans 'Suchradius' %}</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{{ search_subscription.sex }}</td>
|
|
||||||
<td>{{ search_subscription.location }}</td>
|
|
||||||
<td>{{ search_subscription.max_distance }}km</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
@ -1,88 +1,15 @@
|
|||||||
{% extends "fellchensammlung/base_generic.html" %}
|
{% extends "fellchensammlung/base_generic.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% get_current_language as LANGUAGE_CODE_CURRENT %}
|
<form class="form-search card" method="post">
|
||||||
<div class="container-cards">
|
{% csrf_token %}
|
||||||
<form class="form-search card half" method="post">
|
<input type="hidden" name="longitude" maxlength="200" id="longitude">
|
||||||
{% csrf_token %}
|
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
||||||
<input type="hidden" name="longitude" maxlength="200" id="longitude">
|
{{ search_form.as_p }}
|
||||||
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
<input class="btn" type="submit" value="Search" name="search">
|
||||||
<input type="hidden" id="place_id" name="place_id">
|
</form>
|
||||||
{{ search_form.as_p }}
|
{% if place_not_found %}
|
||||||
<ul id="results"></ul>
|
<p class="error">{% translate "Ort nicht gefunden" %}</p>
|
||||||
<div class="container-edit-buttons">
|
{% endif %}
|
||||||
<button class="btn" type="submit" value="search" name="search">
|
|
||||||
<i class="fas fa-search"></i> {% trans 'Suchen' %}
|
|
||||||
</button>
|
|
||||||
{% if searched %}
|
|
||||||
{% if subscribed_search %}
|
|
||||||
<button class="btn" type="submit" value="{{ subscribed_search.pk }}"
|
|
||||||
name="unsubscribe_to_search">
|
|
||||||
<i class="fas fa-bell-slash"></i> {% trans 'Suche nicht mehr abonnieren' %}
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="btn" type="submit" name="subscribe_to_search">
|
|
||||||
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if place_not_found %}
|
|
||||||
<p class="error">
|
|
||||||
{% trans 'Ort nicht gefunden' %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
<div class="card half">
|
|
||||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||||
|
|
||||||
<script>
|
|
||||||
const locationInput = document.getElementById('id_location_string');
|
|
||||||
const resultsList = document.getElementById('results');
|
|
||||||
const placeIdInput = document.getElementById('place_id');
|
|
||||||
|
|
||||||
locationInput.addEventListener('input', async function () {
|
|
||||||
const query = locationInput.value.trim();
|
|
||||||
|
|
||||||
if (query.length < 3) {
|
|
||||||
resultsList.innerHTML = ''; // Don't search for or show results if input is less than 3 characters
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`{{ geocoding_api_url }}/?q=${encodeURIComponent(query)}&limit=5&lang={{ LANGUAGE_CODE_CURRENT }}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data && data.features) {
|
|
||||||
resultsList.innerHTML = ''; // Clear previous results
|
|
||||||
|
|
||||||
const locations = data.features.slice(0, 5); // Show only the first 5 results
|
|
||||||
|
|
||||||
locations.forEach(location => {
|
|
||||||
const listItem = document.createElement('li');
|
|
||||||
listItem.classList.add('result-item');
|
|
||||||
listItem.textContent = geojson_to_summary(location);
|
|
||||||
|
|
||||||
// Add event when user clicks on a result location
|
|
||||||
listItem.addEventListener('click', () => {
|
|
||||||
|
|
||||||
locationInput.value = geojson_to_searchable_string(location); // Set input field to selected location
|
|
||||||
resultsList.innerHTML = ''; // Clear the results after selecting a location
|
|
||||||
});
|
|
||||||
|
|
||||||
resultsList.appendChild(listItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching location data:', error);
|
|
||||||
resultsList.innerHTML = '<li class="result-item">Error fetching data. Please try again.</li>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,16 +3,32 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% translate "Aktualitätscheck" %}</h1>
|
<h1>{% translate "Aktualitätscheck" %}</h1>
|
||||||
<p>{% translate "Überprüfe ob Vermittlungen noch aktuell sind" %}</p>
|
<p>{% translate "Überprüfe ob Vermittlungen noch aktuell sind" %}</p>
|
||||||
<div class="container-cards spaced">
|
{% for adoption_notice in adoption_notices %}
|
||||||
<h1>{% translate 'Deaktivierte Vermittlungen zur Überprüfung' %}</h1>
|
<div class="card">
|
||||||
{% for adoption_notice in adoption_notices_disabled %}
|
<h1>
|
||||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||||
{% endfor %}
|
</h1>
|
||||||
</div>
|
{% if adoption_notice.further_information %}
|
||||||
<div class="container-cards spaced">
|
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||||
<h1>{% translate 'Aktive Vermittlungen zur Überprüfung' %}</h1>
|
{% endif %}
|
||||||
{% for adoption_notice in adoption_notices_active %}
|
<div>
|
||||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
<form method="post">
|
||||||
{% endfor %}
|
{% csrf_token %}
|
||||||
</div>
|
<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 %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,7 +4,6 @@ from django import template
|
|||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from notfellchen import settings
|
from notfellchen import settings
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@ -57,17 +56,6 @@ def pointdecimal(value):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return value
|
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
|
@register.simple_tag
|
||||||
def settings_value(name):
|
def settings_value(name):
|
||||||
return getattr(settings, name)
|
return getattr(settings, name)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user