Compare commits

...

17 Commits

Author SHA1 Message Date
c9f46d7547 ci: fix?
All checks were successful
ci/woodpecker/push/docs Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
2025-01-19 07:12:09 +01:00
9f23f5768c ci: restructure
Some checks failed
ci/woodpecker/push/docs Pipeline failed
ci/woodpecker/push/test Pipeline failed
2025-01-19 07:07:53 +01:00
19210f90cd ci: try teests 2025-01-19 07:04:22 +01:00
462bb8f485 refactor: formatting 2025-01-18 21:43:37 +01:00
ea4d15b99a tests: Add test for index view 2025-01-18 21:43:00 +01:00
de30dfcb8b tests: Add test for unsubscribe 2025-01-18 21:39:45 +01:00
36a979954c feat: make sure owner also gets notified, add test 2025-01-18 18:53:55 +01:00
71ef17dc97 feat: Move pytest, coverage and model bakery to develop dependencies 2025-01-18 18:30:02 +01:00
206cd282e6 feat: Add coverage report instructions 2025-01-18 18:29:04 +01:00
e399346c3e feat: Add tests for subscription functionality 2025-01-18 16:17:22 +01:00
929c6dfff0 feat: Add registration button 2025-01-18 15:47:07 +01:00
841b57fea2 feat: Make sure subscribe leads to login flow 2025-01-18 15:25:27 +01:00
9e5446ff1d feat: Test adding a adoption as user 2025-01-18 15:22:08 +01:00
3b79809b8c docs: various 2025-01-18 09:07:33 +01:00
53e6db3655 refactor: Remove print 2025-01-14 07:31:00 +01:00
424f91e919 feat: Add a timestamp for when a notification is read 2025-01-14 07:30:36 +01:00
84ce5f54b2 refactor: Remove unnecessary print 2025-01-14 07:27:03 +01:00
23 changed files with 332 additions and 48 deletions

4
.coveragerc Normal file
View File

@ -0,0 +1,4 @@
[run]
omit =
*/migrations/*
*/tests/*

View File

@ -6,6 +6,9 @@ steps:
commands: commands:
- cd docs && make html - cd docs && make html
when:
event: [ tag, push ]
deploy: deploy:
image: appleboy/drone-scp image: appleboy/drone-scp
settings: settings:
@ -19,6 +22,8 @@ steps:
source: docs/_build/html/ source: docs/_build/html/
key: key:
from_secret: ssh_key from_secret: ssh_key
when:
event: [ tag, push ]

14
.woodpecker/test.yml Normal file
View File

@ -0,0 +1,14 @@
---
steps:
test:
image: python
commands:
- python -m pip install '.[develop]'
- coverage run --source='.' src/manage.py test src && coverage html
- coverage html
- cat htmlcov/index.html
when:
event: [tag, push]

View File

@ -77,6 +77,26 @@ docker push moanos/notfellchen:latest
docker run -p8000:7345 moanos/notfellchen:latest docker run -p8000:7345 moanos/notfellchen:latest
``` ```
## Testing
Tests can be run with
```zsh
nf test src
```
If you want to report on code coverage run
```zsh
coverage run --source='.' src/manage.py test src
```
and
```
coverage report
```
## 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

View File

@ -36,6 +36,11 @@ An application can then send this token in the request header for authorization.
Endpoints Endpoints
--------- ---------
All Endpoints are documented at https://notfellchen.org/api/schema/swagger-ui/ or at https://notfellchen.org/api/schema/redoc/ if you prefer redoc.
The OpenAI schema can be downloaded at https://notfellchen.org/api/schema/
Examples are documented here.
Get Adoption Notices Get Adoption Notices
++++++++++++++++++++ ++++++++++++++++++++

View File

@ -5,7 +5,7 @@ Report a bug
^^^^^^^^^^^^ ^^^^^^^^^^^^
To report a bug, file an issue on `Github To report a bug, file an issue on `Github
<https://codeberg.org/moanos/notfellchen/issues>`_ <https://github.com/moan0s/notfellchen/issues>`_
Try to include the following information: Try to include the following information:
@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a
pull request. pull request.
If you want to know what can be done, have a look at the current `Github If you want to know what can be done, have a look at the current `Github
<https://codeberg.org/moanos/notfellchen/issues>`_. <https://github.com/moan0s/notfellchen/issues>`_.
Get in touch! Get in touch!
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -5,8 +5,7 @@ What qualifies as release?
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
A new release should be announced when a significant number functions, bugfixes or other improvements to the software A new release should be announced when a significant number functions, bugfixes or other improvements to the software
is made. Usually this indicates a minor release. is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
Major releases are yet to be determined.
What should be done before a release? What should be done before a release?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -14,7 +13,7 @@ What should be done before a release?
Tested basic functions Tested basic functions
###################### ######################
Run :command:`pytest` Run :command:`nf test src`
Test upgrade on a copy of a production database Test upgrade on a copy of a production database
############################################### ###############################################
@ -38,4 +37,4 @@ Do a final commit on this change, and tag the commit as release with appropriate
git tag -a v1.0.0 -m "Releasing version v1.0.0" git tag -a v1.0.0 -m "Releasing version v1.0.0"
git push origin v1.0.0 git push origin v1.0.0
Make sure the tag is visible on Codeberg and celebrate 🥳 Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳

View File

@ -8,13 +8,13 @@ build-backend = "setuptools.build_meta"
name = "notfellchen" name = "notfellchen"
description = "A tool to help." description = "A tool to help."
authors = [ authors = [
{name = "moanos", email = "julian-samuel@gebuehr.net"}, { name = "moanos", email = "julian-samuel@gebuehr.net" },
] ]
maintainers = [ maintainers = [
{name = "moanos", email = "julian-samuel@gebuehr.net"}, { name = "moanos", email = "julian-samuel@gebuehr.net" },
] ]
keywords = ["animal", "adoption", "django", "rescue", ] keywords = ["animal", "adoption", "django", "rescue", ]
license = {text = "AGPL-3.0-or-later"} license = { text = "AGPL-3.0-or-later" }
classifiers = [ classifiers = [
"Environment :: Web", "Environment :: Web",
"License :: OSI Approved :: GNU Affero General Public License v3", "License :: OSI Approved :: GNU Affero General Public License v3",
@ -24,14 +24,12 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"Django", "Django",
"coverage",
"codecov", "codecov",
"sphinx", "sphinx",
"sphinx-rtd-theme", "sphinx-rtd-theme",
"gunicorn", "gunicorn",
"fontawesomefree", "fontawesomefree",
"whitenoise", "whitenoise",
"model_bakery",
"markdown", "markdown",
"Pillow", "Pillow",
"django-registration", "django-registration",
@ -47,7 +45,9 @@ dynamic = ["version", "readme"]
[project.optional-dependencies] [project.optional-dependencies]
develop = [ develop = [
"pytest", "pytest",
"coverage",
"model_bakery",
] ]
[project.urls] [project.urls]
@ -62,6 +62,6 @@ nf = 'notfellchen.main:main'
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = {attr = "notfellchen.__version__"} version = { attr = "notfellchen.__version__" }
readme = {file = "README.md"} readme = { file = "README.md" }

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-01-14 06:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0035_alter_image_alt_text_and_more'),
]
operations = [
migrations.AddField(
model_name='basenotification',
name='read_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-01-14 06:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0036_basenotification_read_at'),
]
operations = [
migrations.AlterField(
model_name='basenotification',
name='title',
field=models.CharField(max_length=100, verbose_name='Titel'),
),
]

View File

@ -818,7 +818,8 @@ 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) updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=100) read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
title = models.CharField(max_length=100, verbose_name=_("Titel"))
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'))
read = models.BooleanField(default=False) read = models.BooleanField(default=False)
@ -829,6 +830,11 @@ class BaseNotification(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
self.user.get_notifications_url() self.user.get_notifications_url()
def mark_read(self):
self.read = True
self.read_at = timezone.now()
self.save()
class CommentNotification(BaseNotification): class CommentNotification(BaseNotification):
comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort')) comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))

View File

@ -259,6 +259,11 @@ a.btn, a.btn2, a.nav-link {
border: 1px solid black; border: 1px solid black;
} }
.btn-small {
font-size: medium;
padding: 6px;
}
.checkmark { .checkmark {
display: inline-block; display: inline-block;
position: relative; position: relative;

View File

@ -7,7 +7,7 @@ from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.misc import healthcheck_ok from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp from .models import Location, AdoptionNotice, Timestamp
from .tools.notifications import notify_moderators_of_AN_to_be_checked from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers from .tools.search import notify_search_subscribers
@ -46,7 +46,7 @@ def post_adoption_notice_save(pk):
logging.info(f"Location was added to Adoption notice {pk}") logging.info(f"Location was added to Adoption notice {pk}")
notify_search_subscribers(instance, only_if_active=True) notify_search_subscribers(instance, only_if_active=True)
notify_moderators_of_AN_to_be_checked(instance) notify_of_AN_to_be_checked(instance)
@celery_app.task(name="tools.healthcheck") @celery_app.task(name="tools.healthcheck")
def task_healthcheck(): def task_healthcheck():

View File

@ -137,7 +137,6 @@ class GeoAPI:
result = self.requests.get(self.api_url, result = self.requests.get(self.api_url,
{"q": location_string, "lang": language}, {"q": location_string, "lang": language},
headers=self.headers).json() headers=self.headers).json()
logging.warning(result)
geofeatures = GeoFeature.geofeatures_from_photon_result(result) geofeatures = GeoFeature.geofeatures_from_photon_result(result)
else: else:
raise NotImplementedError raise NotImplementedError

View File

@ -1,11 +1,13 @@
from fellchensammlung.models import User, AdoptionNoticeNotification, TrustLevel from fellchensammlung.models import User, AdoptionNoticeNotification, TrustLevel
def notify_moderators_of_AN_to_be_checked(adoption_notice): def notify_of_AN_to_be_checked(adoption_notice):
if adoption_notice.is_disabled_unchecked: if adoption_notice.is_disabled_unchecked:
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR): users_to_notify = set(User.objects.filter(trust_level__gt=TrustLevel.MODERATOR))
users_to_notify.add(adoption_notice.owner)
for user in users_to_notify:
AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice, AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice,
user=moderator, user=user,
title=f" Prüfe Vermittlung {adoption_notice}", title=f" Prüfe Vermittlung {adoption_notice}",
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.", text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
) )

View File

@ -130,6 +130,8 @@ def adoption_notice_detail(request, adoption_notice_id):
if action == "unsubscribe": if action == "unsubscribe":
subscription.delete() subscription.delete()
is_subscribed = False is_subscribed = False
elif action == "subscribe":
return redirect_to_login(next=request.path)
else: else:
raise PermissionDenied raise PermissionDenied
else: else:
@ -481,13 +483,11 @@ def my_profile(request):
notification = CommentNotification.objects.get(pk=notification_id) notification = CommentNotification.objects.get(pk=notification_id)
except CommentNotification.DoesNotExist: except CommentNotification.DoesNotExist:
notification = BaseNotification.objects.get(pk=notification_id) notification = BaseNotification.objects.get(pk=notification_id)
notification.read = True notification.mark_read()
notification.save()
elif action == "notification_mark_all_read": elif action == "notification_mark_all_read":
notifications = CommentNotification.objects.filter(user=request.user, mark_read=False) notifications = CommentNotification.objects.filter(user=request.user, mark_read=False)
for notification in notifications: for notification in notifications:
notification.read = True notification.mark_read()
notification.save()
elif action == "search_subscription_delete": elif action == "search_subscription_delete":
search_subscription_id = request.POST.get("search_subscription_id") search_subscription_id = request.POST.get("search_subscription_id")
SearchSubscription.objects.get(pk=search_subscription_id).delete() SearchSubscription.objects.get(pk=search_subscription_id).delete()

View File

@ -3,25 +3,32 @@
{% block content %} {% block content %}
{% if form.errors %} {% if form.errors %}
<p>{% translate "Dein Username oder Passwort ist falsch." %}</p> <p>{% translate "Dein Username oder Passwort ist falsch." %}</p>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p>{% translate "Du bist bereits eingeloggt." %}</p> <p>{% translate "Du bist bereits eingeloggt." %}</p>
{% else %} {% if next %} {% else %} {% if next %}
<p>{% translate "Bitte log dich ein um diese Seite sehen zu können." %}</p> <p class="card">{% translate "Bitte log dich ein um diese Seite sehen zu können." %}</p>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if not user.is_authenticated %} {% if not user.is_authenticated %}
<form class="card" method="post" action="{% url 'login' %}"> <div class="card">
{% csrf_token %} <div class="container-edit-buttons">
{{ form.as_p }} <form method="post" action="{% url 'login' %}">
<input class="btn" type="submit" value={% translate "Einloggen" %} /> {% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" /> {{ form.as_p }}
</form> <input class="btn" type="submit" value="{% translate 'Einloggen' %}"/>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
</div>
<p><a class="btn2" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a></p> <div class="container-edit-buttons">
{% endif %} <a class="btn btn-small" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a>
<a class="btn btn-small" href="{% url 'django_registration_register' %}">{% translate "Registrieren" %}</a>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -27,7 +27,6 @@ class DistanceTest(TestCase):
l_stuttgart = LocationProxy("Stuttgart") l_stuttgart = LocationProxy("Stuttgart")
l_tue = LocationProxy("Tübingen") l_tue = LocationProxy("Tübingen")
# Should be 30km # Should be 30km
print(f"{l_stuttgart.position} -> {l_tue.position}")
distance_tue_stuttgart = calculate_distance_between_coordinates(l_stuttgart.position, l_tue.position) distance_tue_stuttgart = calculate_distance_between_coordinates(l_stuttgart.position, l_tue.position)
self.assertLess(distance_tue_stuttgart, 50) self.assertLess(distance_tue_stuttgart, 50)
self.assertGreater(distance_tue_stuttgart, 20) self.assertGreater(distance_tue_stuttgart, 20)

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from django.test import TestCase from django.test import TestCase
from model_bakery import baker from model_bakery import baker
from fellchensammlung.models import Announcement, Language, User, TrustLevel from fellchensammlung.models import Announcement, Language, User, TrustLevel, BaseNotification
class UserTest(TestCase): class UserTest(TestCase):
@ -77,3 +77,21 @@ class AnnouncementTest(TestCase):
self.assertTrue(self.announcement2 not in active_announcements) self.assertTrue(self.announcement2 not in active_announcements)
self.assertTrue(self.announcement4 not in active_announcements) self.assertTrue(self.announcement4 not in active_announcements)
self.assertTrue(self.announcement5 in active_announcements) self.assertTrue(self.announcement5 in active_announcements)
class TestNotifications(TestCase):
@classmethod
def setUpTestData(cls):
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
def test_mark_read(self):
not1 = BaseNotification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
not2 = BaseNotification.objects.create(user=self.test_user_1,
text="New wombat to adopt", title="🔔 New Wombat alert")
not1.mark_read()
self.assertTrue(not1.read)
self.assertFalse(not2.read)
self.assertTrue((timezone.now() - timedelta(hours=1)) < not1.read_at < timezone.now())
self.assertIsNone(not2.read_at)

View File

@ -0,0 +1,34 @@
from django.test import TestCase
from model_bakery import baker
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, AdoptionNoticeNotification
from fellchensammlung.tools.notifications import notify_of_AN_to_be_checked
class TestNotifications(TestCase):
@classmethod
def setUpTestData(cls):
cls.test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
cls.test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
cls.test_user2 = User.objects.create_user(username='testuser2',
first_name="Miriam",
last_name="Müller",
password='12345')
cls.test_user0.trust_level = TrustLevel.ADMIN
cls.test_user0.save()
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1,)
cls.adoption1.set_unchecked() # Could also emit notification
def test_notify_of_AN_to_be_checked(self):
notify_of_AN_to_be_checked(self.adoption1)
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user0).exists())
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1).exists())
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())

View File

View File

@ -4,7 +4,8 @@ from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions
from fellchensammlung.views import add_adoption_notice from fellchensammlung.views import add_adoption_notice
@ -70,7 +71,33 @@ class AnimalAndAdoptionTest(TestCase):
self.assertTrue(response.status_code < 400) self.assertTrue(response.status_code < 400)
self.assertTrue(AdoptionNotice.objects.get(name="TestAdoption4").is_active) self.assertTrue(AdoptionNotice.objects.get(name="TestAdoption4").is_active)
an = AdoptionNotice.objects.get(name="TestAdoption4")
animals = Animal.objects.filter(adoption_notice=an)
self.assertTrue(len(animals) == 2)
def test_creating_AN_as_user(self):
self.client.login(username='testuser1', password='12345')
form_data = {"name": "TestAdoption5",
"species": Species.objects.first().pk,
"num_animals": "3",
"date_of_birth": "2024-12-04",
"sex": "M",
"group_only": "on",
"searching_since": "2024-11-10",
"location_string": "München",
"description": "Blaaaa",
"further_information": "https://notfellchen.org/",
"save-and-add-another-animal": "Speichern"}
response = self.client.post(reverse('add-adoption'), data=form_data)
self.assertTrue(response.status_code < 400)
self.assertFalse(AdoptionNotice.objects.get(name="TestAdoption5").is_active)
an = AdoptionNotice.objects.get(name="TestAdoption5")
animals = Animal.objects.filter(adoption_notice=an)
self.assertTrue(len(animals) == 3)
self.assertTrue(an.sexes == set("M", ))
class SearchTest(TestCase): class SearchTest(TestCase):
@ -125,8 +152,8 @@ class SearchTest(TestCase):
# We can't use assertContains because TestAdoption3 will always be in response at is included in map # We can't use assertContains because TestAdoption3 will always be in response at is included in map
# In order to test properly, we need to only care for the context that influences the list display # In order to test properly, we need to only care for the context that influences the list display
an_names = [a.name for a in response.context["adoption_notices"]] an_names = [a.name for a in response.context["adoption_notices"]]
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
class UpdateQueueTest(TestCase): class UpdateQueueTest(TestCase):
@ -196,3 +223,64 @@ class UpdateQueueTest(TestCase):
self.assertFalse(self.adoption3.is_active) self.assertFalse(self.adoption3.is_active)
self.assertEqual(self.adoption3.adoptionnoticestatus.major_status, AdoptionNoticeStatus.CLOSED) self.assertEqual(self.adoption3.adoptionnoticestatus.major_status, AdoptionNoticeStatus.CLOSED)
class AdoptionDetailTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
test_user0.save()
test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
test_user0.trust_level = TrustLevel.ADMIN
test_user0.save()
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
berlin = Location.get_location_from_string("Berlin")
adoption1.location = berlin
adoption1.save()
stuttgart = Location.get_location_from_string("Stuttgart")
adoption3.location = stuttgart
adoption3.save()
adoption1.set_active()
adoption3.set_active()
adoption2.set_unchecked()
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
data={"action": "subscribe"})
self.assertTrue(Subscriptions.objects.filter(owner__username="testuser0").exists())
def test_unsubscribe(self):
# Make sure subscription exists
an = AdoptionNotice.objects.get(name="TestAdoption1")
user = User.objects.get(username="testuser0")
subscription = Subscriptions.objects.get_or_create(owner=user, adoption_notice=an)
# Unsubscribe
self.client.login(username='testuser0', password='12345')
response = self.client.post(
reverse('adoption-notice-detail', args=str(an.pk)),
data={"action": "unsubscribe"})
self.assertFalse(Subscriptions.objects.filter(owner__username="testuser0").exists())
def test_login_required(self):
response = self.client.post(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
data={"action": "subscribe"})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/vermittlung/1/")

View File

@ -0,0 +1,43 @@
from django.test import TestCase
from django.urls import reverse
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species
from model_bakery import baker
class BasicViewTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
test_user0.trust_level = TrustLevel.ADMIN
test_user0.save()
ans = []
for i in range(0, 8):
ans.append(baker.make(AdoptionNotice, name=f"TestAdoption{i}"))
for i in range(0, 4):
AdoptionNotice.objects.get(name=f"TestAdoption{i}").set_active()
def test_index_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('index'))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "TestAdoption0")
self.assertNotContains(response, "TestAdoption5") # Should not be active, therefore not shown
def test_index_anonymous(self):
response = self.client.get(reverse('index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption4") # Should not be active, therefore not shown