Compare commits

31 Commits

Author SHA1 Message Date
a372be4af2 fix: don't use search when checking specialized rescue orgs 2025-08-12 06:16:27 +02:00
5d333b28ab feat: Fix pagination when searching 2025-08-12 06:12:27 +02:00
84ad047c01 feat: Add search for rescue orgs 2025-08-12 00:06:42 +02:00
c93b2631cb feat: Add shortcut to open rescue org website 2025-08-11 22:16:26 +02:00
15dd06a91f feat: Add shortcut to mark rescue org as checked 2025-08-11 21:51:30 +02:00
30ff26c7ef feat: Divide adoption notices of org by active and inactive 2025-08-11 12:58:02 +02:00
1434e7502a fix: Limit upload of fediverse images to 6 2025-08-11 12:43:15 +02:00
93b21fb7d0 fix: Only try to access trust level when authenticated 2025-08-10 18:32:15 +02:00
e5c82f392c feat(test): Add test for map and metrics 2025-08-10 18:31:53 +02:00
0626964461 feat(test): Add test for token showing 2025-08-10 18:11:10 +02:00
23a724e390 fix: Ensure users of higher trust level are also allowed 2025-08-10 17:51:25 +02:00
2a9c7cf854 feat(test): Add basic tests for user views 2025-08-10 17:51:05 +02:00
335630e16d feat(test): Add test for search by location 2025-08-10 17:50:17 +02:00
6051f7c294 feat(test): Exclude from coverage check 2025-08-10 10:17:59 +02:00
c1ea6cd211 feat(test): Add AN form to basic check 2025-08-10 08:44:28 +02:00
6c43b46007 refactor: break out search test into own file 2025-08-10 08:43:49 +02:00
dc9e68c4b9 refactor: remoive print 2025-08-10 08:22:57 +02:00
4b03f99971 feat(test): Add rss feed test 2025-08-09 16:46:07 +02:00
426f4b3d8b fix: Make sure e-mail is sent when comment is reported 2025-08-09 12:30:42 +02:00
3604233507 fix (test): Rules are now shown on terms of service page 2025-08-09 12:30:22 +02:00
8c5099f14a fix (test): Notification framework changed 2025-08-09 12:16:13 +02:00
d5bc348453 feat: allow marking read with very ugly double delete class 2025-08-03 10:40:42 +02:00
bce98cb439 trans: Translate models 2025-08-03 10:29:47 +02:00
1ed3d27533 feat: add option to sync to twenty 2025-08-03 10:00:42 +02:00
39a098af8e feat: Add option to mask e-mails and phone numbers
This is a prerequisite to do tests on DEv and UAT systems
2025-08-01 20:28:51 +02:00
62491b84c1 feat(seo): Add basic description 2025-08-01 19:32:02 +02:00
81f7f5bb5d fix: Use correct heading hierarchy 2025-08-01 19:31:37 +02:00
8ce4122160 feat: raise 404 when AN not found 2025-07-30 08:01:34 +02:00
370ad2ce66 feat: add warning when an is waiting for review 2025-07-30 06:57:31 +02:00
f25c425d85 feat: add warning when someone is interested 2025-07-30 06:51:29 +02:00
d921623f31 fix: Make sure content class is used when rendering markdown 2025-07-25 22:38:49 +02:00
42 changed files with 904 additions and 229 deletions

View File

@@ -147,3 +147,11 @@ class AdoptionNoticeSearchForm(forms.Form):
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
class RescueOrgSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))

View File

@@ -17,7 +17,7 @@ def notify_mods_new_report(report, notification_type):
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
title = _("Vermittlung gemeldet")
elif notification_type == NotificationTypeChoices.NEW_COMMENT:
elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT:
title = _("Kommentar gemeldet")
else:
raise NotImplementedError

View File

@@ -0,0 +1,13 @@
from django.core.management import BaseCommand
from fellchensammlung.tools.admin import mask_organization_contact_data
class Command(BaseCommand):
help = 'Mask e-mail addresses and phone numbers of organizations for testing purposes.'
def add_arguments(self, parser):
parser.add_argument("domain", type=str)
def handle(self, *args, **options):
domain = options["domain"]
mask_organization_contact_data(domain)

View File

@@ -0,0 +1,19 @@
from django.core.management import BaseCommand
from tqdm import tqdm
from fellchensammlung.models import RescueOrganization
from fellchensammlung.tools.twenty import sync_rescue_org_to_twenty
class Command(BaseCommand):
help = 'Send rescue organizations as companies to twenty'
def add_arguments(self, parser):
parser.add_argument("base_url", type=str)
parser.add_argument("token", type=str)
def handle(self, *args, **options):
base_url = options["base_url"]
token = options["token"]
for rescue_org in tqdm(RescueOrganization.objects.all()):
sync_rescue_org_to_twenty(rescue_org, base_url, token)

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-08-02 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0058_socialmediapost'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='twenty_id',
field=models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID'),
),
migrations.AlterField(
model_name='rescueorganization',
name='specializations',
field=models.ManyToManyField(blank=True, to='fellchensammlung.species'),
),
]

View File

@@ -60,6 +60,10 @@ class Location(models.Model):
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("Standort")
verbose_name_plural = _("Standorte")
def __str__(self):
if self.city and self.postcode:
return f"{self.city} ({self.postcode})"
@@ -103,6 +107,10 @@ class Location(models.Model):
class ImportantLocation(models.Model):
class Meta:
verbose_name = _("Wichtiger Standort")
verbose_name_plural = _("Wichtige Standorte")
location = models.OneToOneField(Location, on_delete=models.CASCADE)
slug = models.SlugField(unique=True)
name = models.CharField(max_length=200)
@@ -173,10 +181,14 @@ class RescueOrganization(models.Model):
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
# allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True)
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
help_text=_("ID der der Organisation in Twenty"))
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
ordering = ['name']
verbose_name = _("Tierschutzorganisation")
verbose_name_plural = _("Tierschutzorganisationen")
def __str__(self):
return f"{self.name}"
@@ -204,6 +216,18 @@ class RescueOrganization(models.Model):
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
return adoption_notices_discovered
@property
def adoption_notices_in_hierarchy_divided_by_status(self):
"""Returns two lists of adoption notices, the first active, the other inactive."""
active_adoption_notices = []
inactive_adoption_notices = []
for an in self.adoption_notices_in_hierarchy:
if an.is_active:
active_adoption_notices.append(an)
else:
inactive_adoption_notices.append(an)
return active_adoption_notices, inactive_adoption_notices
@property
def position(self):
if self.location:
@@ -248,6 +272,14 @@ class RescueOrganization(models.Model):
def child_organizations(self):
return RescueOrganization.objects.filter(parent_org=self)
def in_distance(self, position, max_distance, unknown_true=True):
"""
Returns a boolean indicating if the Location of the adoption notice is within a given distance to the position
If the location is none, we by default return that the location is within the given distance
"""
return geo.object_in_distance(self, position, max_distance, unknown_true)
# Admins can perform all actions and have the highest trust associated with them
# Moderators can make moderation decisions regarding the deletion of content
@@ -325,6 +357,10 @@ class Image(models.Model):
def __str__(self):
return self.alt_text
class Meta:
verbose_name = _("Bild")
verbose_name_plural = _("Bilder")
@property
def as_html(self):
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
@@ -335,11 +371,11 @@ class AdoptionNotice(models.Model):
permissions = [
("create_active_adoption_notice", "Can create an active adoption notice"),
]
verbose_name = _("Vermittlung")
verbose_name_plural = _("Vermittlungen")
def __str__(self):
if not hasattr(self, 'adoptionnoticestatus'):
return self.name
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
return self.name
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
@@ -472,11 +508,7 @@ class AdoptionNotice(models.Model):
If the location is none, we by default return that the location is within the given distance
"""
if unknown_true and self.position is None:
return True
distance = geo.calculate_distance_between_coordinates(self.position, position)
return distance < max_distance
return geo.object_in_distance(self, position, max_distance, unknown_true)
@property
def is_active(self):
@@ -496,6 +528,18 @@ class AdoptionNotice(models.Model):
return False
return self.adoptionnoticestatus.is_closed
@property
def is_interested(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_interested
@property
def is_awaiting_action(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_awaiting_action
@property
def is_disabled_unchecked(self):
if not hasattr(self, 'adoptionnoticestatus'):
@@ -545,6 +589,10 @@ class AdoptionNoticeStatus(models.Model):
whereas the minor status is used for reporting
"""
class Meta:
verbose_name = _('Vermittlungsstatus')
verbose_name_plural = _('Vermittlungsstati')
ACTIVE = "active"
AWAITING_ACTION = "awaiting_action"
CLOSED = "closed"
@@ -609,6 +657,14 @@ class AdoptionNoticeStatus(models.Model):
def is_closed(self):
return self.major_status == self.CLOSED
@property
def is_awaiting_action(self):
return self.major_status == self.AWAITING_ACTION
@property
def is_interested(self):
return self.major_status == self.ACTIVE and self.minor_status == "interested"
@property
def is_disabled_unchecked(self):
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
@@ -663,6 +719,10 @@ class SexChoicesWithAll(models.TextChoices):
class Animal(models.Model):
class Meta:
verbose_name = _('Tier')
verbose_name_plural = _('Tiere')
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
name = models.CharField(max_length=200)
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
@@ -728,6 +788,11 @@ class SearchSubscription(models.Model):
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
- For matches: Send notification to user of the SearchSubscription
"""
class Meta:
verbose_name = _("Abonnierte Suche")
verbose_name_plural = _("Abonnierte Suchen")
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)
@@ -746,6 +811,11 @@ class Rule(models.Model):
"""
Class to store rules
"""
class Meta:
verbose_name = _("Regel")
verbose_name_plural = _("Regeln")
title = models.CharField(max_length=200)
# Markdown is allowed in rule text
@@ -762,7 +832,8 @@ class Rule(models.Model):
class Report(models.Model):
class Meta:
permissions = []
verbose_name = _("Meldung")
verbose_name_plural = _("Meldungen")
ACTION_TAKEN = "action taken"
NO_ACTION_TAKEN = "no action taken"
@@ -842,6 +913,10 @@ class ReportComment(Report):
class ModerationAction(models.Model):
class Meta:
verbose_name = _("Moderationsaktion")
verbose_name_plural = _("Moderationsaktionen")
BAN = "user_banned"
DELETE = "content_deleted"
COMMENT = "comment"
@@ -906,6 +981,11 @@ class Announcement(Text):
"""
Class to store announcements that should be displayed for all users
"""
class Meta:
verbose_name = _("Banner")
verbose_name_plural = _("Banner")
logged_in_only = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -955,6 +1035,11 @@ class Comment(models.Model):
"""
Class to store comments in markdown content
"""
class Meta:
verbose_name = _("Kommentar")
verbose_name_plural = _("Kommentare")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -974,6 +1059,10 @@ class Comment(models.Model):
class Notification(models.Model):
class Meta:
verbose_name = _("Benachrichtigung")
verbose_name_plural = _("Benachrichtigungen")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
notification_type = models.CharField(max_length=200,
@@ -1017,6 +1106,12 @@ class Notification(models.Model):
class Subscriptions(models.Model):
"""Subscription to a AdoptionNotice"""
class Meta:
verbose_name = _("Abonnement")
verbose_name_plural = _("Abonnements")
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
created_at = models.DateTimeField(auto_now_add=True)
@@ -1044,6 +1139,11 @@ class Timestamp(models.Model):
"""
Class to store timestamps based on keys
"""
class Meta:
verbose_name = _("Zeitstempel")
verbose_name_plural = _("Zeitstempel")
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)
@@ -1056,6 +1156,11 @@ class SpeciesSpecificURL(models.Model):
"""
Model that allows to specify a URL for a rescue organization where a certain species can be found
"""
class Meta:
verbose_name = _("Tierartspezifische URL")
verbose_name_plural = _("Tierartspezifische URLs")
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))

View File

@@ -0,0 +1,11 @@
/* mousetrap v1.6.5 craig.is/killing/mice */
(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||
"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=
a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter",
escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};
this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};
d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);

View File

@@ -0,0 +1,15 @@
function mark_checked(index) {
document.getElementById('mark_checked_'+index).submit();
}
function open_information(index) {
let link = document.getElementById('species_url_'+index+'_1');
if (!link) {
link = document.getElementById('rescue_org_website_'+index);
}
window.open(link.href);
}
Mousetrap.bind('c', function() { mark_checked(1); });
Mousetrap.bind('o', function() { open_information(1); });

View File

@@ -22,7 +22,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Looks for all notifications with a delete and allows closing them when pressing delete
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
(document.querySelectorAll('.notification .delete:not(.js-delete-excluded)') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {

View File

@@ -30,7 +30,7 @@
<div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2>
</div>
<div class="card-content">
<div class="card-content content">
{{ faq.content | render_markdown }}
</div>
</div>

View File

@@ -16,11 +16,27 @@
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
{% block content %}
<div class="block">
<div style="height: 70vh">
{% include "fellchensammlung/partials/partial-map.html" %}
<div class="columns block">
<div class=" column {% if show_search %}is-two-thirds {% endif %}">
<div style="height: 70vh">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% if show_search %}
<div class="column is-one-third">
<form method="GET" autocomplete="off">
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form }}
<button class="button is-primary is-fullwidth" type="submit" value="search" name="action">
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
</button>
</form>
</div>
{% endif %}
</div>
<div class="block">
{% with rescue_organizations=rescue_organizations_to_list %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
@@ -29,16 +45,17 @@
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
{% if rescue_organizations_to_list.has_previous %}
<a class="pagination-previous"
href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %}</a>
href="?page={% url_replace request 'page' rescue_organizations_to_list.previous_page_number %}">{% trans 'Vorherige' %}</a>
{% endif %}
{% if rescue_organizations_to_list.has_next %}
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
<a class="pagination-next"
href="?{% url_replace request 'page' rescue_organizations_to_list.next_page_number %}">{% trans 'Nächste' %}</a>
{% endif %}
<ul class="pagination-list">
{% for page in elided_page_range %}
{% if page != "…" %}
<li>
<a href="?page={{ page }}"
<a href="?{% url_replace request 'page' page %}"
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
{{ page }}

View File

@@ -26,20 +26,7 @@
{% endblock %}
{% block content %}
{% if adoption_notice.is_closed %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Vermittlung deaktiviert' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
vermittelt wurden.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
</div>
</article>
{% endif %}
{% include "fellchensammlung/partials/partial-adoption-notice-status.html" %}
<div class="columns">
<div class="column is-two-thirds">
<!--- Title level (including action dropdown) -->
@@ -211,9 +198,9 @@
<div class="column block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-4">{% translate "Beschreibung" %}</h1>
<h4 class="card-header-title title is-4">{% translate "Beschreibung" %}</h4>
</div>
<div class="card-content">
<div class="card-content content">
<p class="expandable">{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}

View File

@@ -32,7 +32,9 @@
{{ org.location_string }}
{% endif %}
{% if org.description %}
<p>{{ org.description | render_markdown }}</p>
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
@@ -52,7 +54,8 @@
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
@@ -77,7 +80,7 @@
{% if org.child_organizations %}
<div class="block">
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
{% with rescue_organizations=org.child_organizations %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
{% endwith %}
@@ -86,13 +89,32 @@
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
<div class="container-cards">
{% if org.adoption_notices_in_hierarchy %}
{% for adoption_notice in org.adoption_notices_in_hierarchy %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
{% with ans_by_status=org.adoption_notices_in_hierarchy_divided_by_status %}
{% with active_ans=ans_by_status.0 inactive_ans=ans_by_status.1 %}
<div class="block">
<h3 class="title is-3">{% translate 'Aktive Vermittlungen' %}</h3>
<div class="container-cards">
{% if active_ans %}
{% for adoption_notice in active_ans %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
</div>
<div class="block">
<h3 class="title is-3">{% translate 'Inaktive Vermittlungen' %}</h3>
<div class="container-cards">
{% if inactive_ans %}
{% for adoption_notice in inactive_ans %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
</div>
{% endwith %}
{% endwith %}
{% endblock %}

View File

@@ -8,7 +8,7 @@
<h1 class="message-header">
{{ external_site_warning.title }}
</h1>
<div class="message-body">
<div class="message-body content">
{{ external_site_warning.content | render_markdown }}
</div>
{% else %}

View File

@@ -92,17 +92,19 @@
{% translate 'Tierheime in der Nähe' %}
</a>
<br/>
{% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %}
<a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %}
</a>
{% endif %}
<br/>
{% if request.user.is_superuser %}
<a class="nav-link " href="{% url "admin:index" %}">
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
</a>
{% if request.user.is_authenticated %}
{% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %}
<a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %}
</a>
{% endif %}
<br/>
{% if request.user.is_superuser %}
<a class="nav-link " href="{% url "admin:index" %}">
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
</a>
{% endif %}
{% endif %}
</div>
</div>

View File

@@ -1,6 +1,9 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block description %}
<meta name="description" content="{% trans 'Inhalt melden' %}">
{% endblock %}
{% block content %}
<h1 class="title is-1">{% translate "Melden" %}</h1>
Wenn dieser Inhalt nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.

View File

@@ -23,7 +23,9 @@
{% endfor %}
{% if introduction %}
<h1>{{ introduction.title }}</h1>
{{ introduction.content | render_markdown }}
<div class="content">
{{ introduction.content | render_markdown }}
</div>
{% endif %}
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
@@ -44,7 +46,7 @@
<h2 class="title is-1">{{ how_to.title }}</h2>
</div>
</div>
<div class="card-content">
<div class="card-content content">
{{ how_to.content | render_markdown }}
</div>
</div>

View File

@@ -71,7 +71,9 @@
</div>
{% else %}
{% if adoption_notice.description_short %}
{{ adoption_notice.description_short | render_markdown }}
<div class="content">
{{ adoption_notice.description_short | render_markdown }}
</div>
{% endif %}
{% endif %}
</div>

View File

@@ -0,0 +1,43 @@
{% load custom_tags %}
{% load i18n %}
{% if adoption_notice.is_closed %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Vermittlung deaktiviert' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
vermittelt wurden.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
</div>
</article>
{% elif adoption_notice.is_interested %}
<article class="message is-info">
<div class="message-header">
<p>{% translate 'Tiere sind reserviert' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Tiere sind bereits reserviert.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
</div>
</article>
{% elif adoption_notice.is_awaiting_action %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Warten auf Aktivierung' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung muss noch durch Moderator*innen aktiviert werden und taucht daher nicht auf der
Startseite auf.
Ggf. fehlen noch relevante Informationen.
{% endblocktranslate %}
</div>
</article>
{% endif %}

View File

@@ -19,13 +19,15 @@
{{ adoption_notice.location_string }}
{% endif %}
</p>
<p>
<div class="content">
{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% translate "Keine Beschreibung" %}
<p>
{% translate "Keine Beschreibung" %}
</p>
{% endif %}
</p>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"

View File

@@ -15,18 +15,27 @@
<strong>{% translate 'Zuletzt geprüft:' %}</strong> {{ rescue_org.last_checked_hr }}
</p>
<p>
<i class="fas fa-images" aria-label="{% translate "Material verwenden?" %}"></i> {{ rescue_org.get_allows_using_materials_display }}
<i class="fas fa-images"
aria-label="{% translate "Material verwenden?" %}"></i> {{ rescue_org.get_allows_using_materials_display }}
</p>
{% if rescue_org.website %}
<a href="{{ rescue_org.website }}" target="_blank">
<a href="{{ rescue_org.website }}" id="rescue_org_website_{{ forloop.counter }}" target="_blank">
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
{{ rescue_org.website|domain }}
</a>
{% endif %}
{% for species_url in rescue_org.species_urls %}
<p>{{ species_url.species }}: <a href="{{ species_url.url }}" target="_blank">{{ species_url.url }}</a>
</p>
{% endfor %}
{% with rescue_org_counter=forloop.counter %}
{% for species_url in rescue_org.species_urls %}
<p>{{ species_url.species }}:
<a href="{{ species_url.url }}"
id="species_url_{{ rescue_org_counter }}_{{ forloop.counter }}"
target="_blank">
{{ species_url.url }}
</a>
</p>
{% endfor %}
{% endwith %}
</div>
{% if set_internal_comment_available %}
<div class="block">
@@ -55,7 +64,7 @@
</div>
<div class="card-footer">
<div class="card-footer-item is-confirm">
<form method="post">
<form method="post" id="mark_checked_{{ forloop.counter }}">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"

View File

@@ -1,12 +1,14 @@
{% load i18n %}
{% load custom_tags %}
<div class="notification">
<form class="delete" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="notification_mark_read">
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
<button class="" type="submit" id="submit"></button>
</form>
<div class="notification {% if not notification.read %}is-info is-light{% endif %}">
{% if not notification.read %}
<form class="delete js-delete-excluded" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="notification_mark_read">
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
<button class="delete js-delete-excluded" type="submit" id="submit"></button>
</form>
{% endif %}
<div class="notification-header">
<a href="{{ notification.url }}"><b>{{ notification.title }}</b></a>
<i class="card-timestamp">{{ notification.created_at|time_since_hr }}</i>

View File

@@ -8,18 +8,18 @@
</div>
<div class="card-content">
<p>
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if rescue_organization.location %}
{{ rescue_organization.location }}
{% else %}
{{ rescue_organization.location_string }}
{% endif %}
</p>
<p>
</div>
<div class="block content">
{% if rescue_organization.description_short %}
{{ rescue_organization.description_short | render_markdown }}
{% endif %}
</p>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<div class="card-header">
<h2 class="card-header-title">{{ rule.title }}</h2>
</div>
<div class="card-content">
<p class="content">{{ rule.rule_text | render_markdown }}</p>
<div class="card-content content">
{{ rule.rule_text | render_markdown }}
</div>
</div>

View File

@@ -1,5 +1,11 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load static %}
{% block additional_scrips %}
<script src="{% static 'fellchensammlung/js/mousetrap.min.js' %}"></script>
<script src="{% static 'fellchensammlung/js/rescue-org-check-shortcuts.js' %}"></script>
{% endblock %}
{% block content %}
<h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1>
<p class="subtitle is-3">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p>

View File

@@ -122,3 +122,12 @@ def host():
def time_since_hr(timestamp):
t_delta = timezone.now() - timestamp
return time_since_as_hr_string(t_delta)
@register.simple_tag
def url_replace(request, field, value):
dict_ = request.GET.copy()
dict_[field] = value
return dict_.urlencode()

View File

@@ -1,5 +1,6 @@
import logging
from random import randint
from notfellchen import settings
from django.utils import timezone
from datetime import timedelta
@@ -139,3 +140,18 @@ def send_test_email(email):
to = email
mail.send_mail(subject, plain_message, from_email, [to], html_message=html_message)
def mask_organization_contact_data(catchall_domain="example.org"):
"""
Masks e-mails, so they are all sent to one domain, preferably a catchall domain.
"""
rescue_orgs_with_phone_number = RescueOrganization.objects.filter(phone_number__isnull=False)
for rescue_org_with_phone_number in rescue_orgs_with_phone_number:
rescue_org_with_phone_number.phone_number = randint(100000000000, 1000000000000)
rescue_org_with_phone_number.save()
rescue_orgs_with_email = RescueOrganization.objects.filter(email__isnull=False)
for rescue_org_with_email in rescue_orgs_with_email:
rescue_org_with_email.email = f"{rescue_org_with_email.email.replace('@', '-')}@{catchall_domain}"
rescue_org_with_email.save()

View File

@@ -58,6 +58,9 @@ class FediClient:
response = requests.post(status_endpoint, headers=self.headers, data=payload)
# Raise exception if posting fails
if response.status_code >= 300:
logging.error(f"Request= {response.request.body}")
logging.error(f"Response= {response.json()}")
response.raise_for_status()
return response.json()
@@ -70,8 +73,11 @@ class FediClient:
:param alt_text: The alt text for the image.
:return: The response from the Mastodon API.
"""
MAX_NUM_OF_IMAGES = 6
if len(images) > MAX_NUM_OF_IMAGES:
logging.warning(f"Too many images ({len(images)}) to post. Selecting the first {MAX_NUM_OF_IMAGES} images.")
media_ids = []
for image in images:
for image in images[:MAX_NUM_OF_IMAGES]:
# Upload the image and get the media ID
media_ids.append(self.upload_media(f"{settings.MEDIA_ROOT}/{image.image}", image.alt_text))

View File

@@ -53,6 +53,19 @@ def calculate_distance_between_coordinates(position1, position2):
return distance_in_km
def object_in_distance(obj, position, max_distance, unknown_true=True):
"""
Returns a boolean indicating if the Location of the object is within a given distance to the position
If the location is none, we by default return that the location is within the given distance
"""
if unknown_true and obj.position is None:
return True
distance = calculate_distance_between_coordinates(obj.position, position)
return distance < max_distance
class ResponseMock:
content = b'[{"place_id":138181499,"licence":"Data \xc2\xa9 OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1247237,"lat":"48.4949904","lon":"9.040330235970146","category":"boundary","type":"postal_code","place_rank":21, "importance":0.12006895017929346,"addresstype":"postcode","name":"72072","display_name":"72072, Derendingen, T\xc3\xbcbingen, Landkreis T\xc3\xbcbingen, Baden-W\xc3\xbcrttemberg, Deutschland", "boundingbox":["48.4949404","48.4950404","9.0402802","9.0403802"]}]'
status_code = 200

View File

@@ -2,9 +2,9 @@ import logging
from django.utils.translation import gettext_lazy as _
from .geo import LocationProxy, Position
from ..forms import AdoptionNoticeSearchForm
from ..forms import AdoptionNoticeSearchForm, RescueOrgSearchForm
from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \
Notification, NotificationTypeChoices
Notification, NotificationTypeChoices, RescueOrganization
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
@@ -18,7 +18,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b
return
for search_subscription in SearchSubscription.objects.all():
logging.debug(f"Search subscription {search_subscription} found.")
search = Search(search_subscription=search_subscription)
search = AdoptionNoticeSearch(search_subscription=search_subscription)
if search.adoption_notice_fits_search(adoption_notice):
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
Notification.objects.create(user_to_notify=search_subscription.owner,
@@ -33,7 +33,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b
logging.info(f"Subscribers for AN {adoption_notice.pk} have been notified\n")
class Search:
class AdoptionNoticeSearch:
def __init__(self, request=None, search_subscription=None):
self.sex = None
self.area_search = None
@@ -45,7 +45,7 @@ class Search:
self.location_string = None
if request:
self.search_from_request(request)
self.adoption_notice_search_from_request(request)
elif search_subscription:
self.search_from_search_subscription(search_subscription)
@@ -103,7 +103,7 @@ class Search:
return adoptions
def search_from_request(self, request):
def adoption_notice_search_from_request(self, request):
if request.method == 'POST':
self.search_form = AdoptionNoticeSearchForm(request.POST)
self.search_form.is_valid()
@@ -157,3 +157,75 @@ class Search:
return False
else:
return True
class RescueOrgSearch:
def __init__(self, request):
self.area_search = None
self.max_distance = None
self.location = None # Can either be Location (DjangoModel) or LocationProxy
self.place_not_found = False # Indicates that a location was given but could not be geocoded
self.search_form = None
# Either place_id or location string must be set for area search
self.location_string = None
self.rescue_org_search_from_request(request)
def __str__(self):
return f"{_('Suche')}: {self.location=}, {self.area_search=}, {self.max_distance=}"
def __eq__(self, other):
"""
Custom equals that also supports SearchSubscriptions
Only allowed to be called for located subscriptions
"""
# If both locations are empty check only the max distance
if self.location is None and other.location is None:
return self.max_distance == other.max_distance
# If one location is empty and the other is not, they are not equal
elif self.location is not None and other.location is None or self.location is None and other.location is not None:
return False
return self.location == other.location and self.max_distance == other.max_distance
def _locate(self):
try:
self.location = LocationProxy(self.location_string)
except ValueError:
self.place_not_found = True
@property
def position(self):
if self.area_search and not self.place_not_found:
return Position(latitude=self.location.latitude, longitude=self.location.longitude)
else:
return None
def rescue_org_fits_search(self, rescue_org: RescueOrganization):
# make sure it's an area search and the place is found to check location
if self.area_search and not self.place_not_found:
# If adoption notice is in not in search distance, return false
if not rescue_org.in_distance(self.location.position, self.max_distance):
logging.debug("Area mismatch")
return False
return True
def get_rescue_orgs(self):
rescue_orgs = RescueOrganization.objects.all()
fitting_rescue_orgs = [rescue_org for rescue_org in rescue_orgs if self.rescue_org_fits_search(rescue_org)]
return fitting_rescue_orgs
def rescue_org_search_from_request(self, request):
if request.method == 'GET' and request.GET.get("action", False) == "search":
self.search_form = RescueOrgSearchForm(request.GET)
self.search_form.is_valid()
if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[
"max_distance"] != "":
self.area_search = True
self.location_string = self.search_form.cleaned_data["location_string"]
self.max_distance = int(self.search_form.cleaned_data["max_distance"])
self._locate()
else:
self.search_form = RescueOrgSearchForm()

View File

@@ -0,0 +1,52 @@
import requests
from fellchensammlung.models import RescueOrganization
def sync_rescue_org_to_twenty(rescue_org: RescueOrganization, base_url, token: str):
if rescue_org.twenty_id:
update = True
else:
update = False
payload = {
"eMails": {
"primaryEmail": rescue_org.email,
"additionalEmails": None
},
"domainName": {
"primaryLinkLabel": rescue_org.website,
"primaryLinkUrl": rescue_org.website,
"additionalLinks": []
},
"name": rescue_org.name,
}
if rescue_org.location:
payload["address"] = {
"addressStreet1": f"{rescue_org.location.street} {rescue_org.location.housenumber}",
"addressCity": rescue_org.location.city,
"addressPostcode": rescue_org.location.postcode,
"addressCountry": rescue_org.location.countrycode,
"addressLat": rescue_org.location.latitude,
"addressLng": rescue_org.location.longitude,
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
if update:
url = f"{base_url}/rest/companies/{rescue_org.twenty_id}"
response = requests.patch(url, json=payload, headers=headers)
assert response.status_code == 200
else:
url = f"{base_url}/rest/companies"
response = requests.post(url, json=payload, headers=headers)
try:
assert response.status_code == 201
except AssertionError:
print(response.request.body)
return
rescue_org.twenty_id = response.json()["data"]["createCompany"]["id"]
rescue_org.save()

View File

@@ -36,7 +36,7 @@ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deacti
from .tasks import post_adoption_notice_save
from rest_framework.authtoken.models import Token
from .tools.search import Search
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
@@ -44,8 +44,11 @@ def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
def user_is_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
"""
Checks if a user is either the owner of a record or has a trust level equal or higher than the given one
"""
return user.is_authenticated and (
user.trust_level == trust_level or django_object.owner == user)
user.trust_level >= trust_level or django_object.owner == user)
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
@@ -100,13 +103,12 @@ def handle_an_check_actions(request, action, adoption_notice=None):
if action == "checked_inactive":
adoption_notice.set_closed()
elif action == "checked_active":
print("dads")
adoption_notice.set_active()
return None
def adoption_notice_detail(request, adoption_notice_id):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
adoption_notice = get_object_or_404(AdoptionNotice, id=adoption_notice_id)
adoption_notice_meta = adoption_notice._meta
if request.user.is_authenticated:
try:
@@ -198,7 +200,7 @@ def adoption_notice_edit(request, adoption_notice_id):
def search_important_locations(request, important_location_slug):
i_location = get_object_or_404(ImportantLocation, slug=important_location_slug)
search = Search()
search = AdoptionNoticeSearch()
search.search_from_predefined_i_location(i_location)
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
@@ -229,8 +231,8 @@ def search(request, templatename="fellchensammlung/search.html"):
# A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button
searched = False
search = Search()
search.search_from_request(request)
search = AdoptionNoticeSearch()
search.adoption_notice_search_from_request(request)
if request.method == 'POST':
searched = True
if "subscribe_to_search" in request.POST:
@@ -569,7 +571,7 @@ def user_detail(request, user, token=None):
def user_by_id(request, user_id):
user = User.objects.get(id=user_id)
# Only users that are mods or owners of the user are allowed to view
fail_if_user_not_owner_or_trust_level(request.user, user)
fail_if_user_not_owner_or_trust_level(user=request.user, django_object=user, trust_level=TrustLevel.MODERATOR)
if user == request.user:
return my_profile(request)
else:
@@ -747,8 +749,12 @@ def external_site_warning(request, template_name='fellchensammlung/external-site
def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'):
if species is None:
rescue_organizations = RescueOrganization.objects.all()
# rescue_organizations = RescueOrganization.objects.all()
org_search = RescueOrgSearch(request)
rescue_organizations = org_search.get_rescue_orgs()
else:
org_search = None
rescue_organizations = RescueOrganization.objects.filter(specializations=species)
paginator = Paginator(rescue_organizations, 10)
@@ -765,7 +771,21 @@ def list_rescue_organizations(request, species=None, template='fellchensammlung/
rescue_organizations_to_list = paginator.get_page(page_number)
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
"show_rescue_orgs": True,
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1)}
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1),
}
if org_search:
additional_context = {
"show_search": True,
"search_form": org_search.search_form,
"place_not_found": org_search.place_not_found,
"map_center": org_search.position,
"search_center": org_search.position,
"map_pins": [org_search],
"location": org_search.location,
"search_radius": org_search.max_distance,
"zoom_level": zoom_level_for_radius(org_search.max_distance),
}
context.update(additional_context)
return render(request, template, context=context)
@@ -875,6 +895,7 @@ def moderation_tools_overview(request):
if action == "post_to_fedi":
adoption_notice = SocialMediaPost.get_an_to_post()
if adoption_notice is not None:
logging.info(f"Posting adoption notice: {adoption_notice} ({adoption_notice.id})")
try:
post = post_an_to_fedi(adoption_notice)
context = {"action_was_posting": True, "post": post, "posted_successfully": True}

View File

@@ -30,6 +30,6 @@ urlpatterns += i18n_patterns(
prefix_default_language=False
)
if settings.DEBUG:
if settings.DEBUG: # pragma: no cover
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

View File

@@ -1,6 +1,14 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block description %}
{% if next %}
<meta name="description" content="{% trans 'Bei Notfellchen.org einloggen' %}">
{% else %}
<meta name="description" content="{% translate "Bitte log dich ein um diese Seite sehen zu können." %}">
{% endif %}
{% endblock %}
{% block content %}
{% if form.errors %}
@@ -14,15 +22,15 @@
{% if user.is_authenticated %}
<p class="is-warning">{% translate "Du bist bereits eingeloggt." %}</p>
{% else %} {% if next %}
<div class="notification is-warning">
<button class="delete"></button>
<p>
{% translate "Bitte log dich ein um diese Seite sehen zu können." %}
</p>
</div>
{% endif %}
{% else %}
{% if next %}
<div class="notification is-warning">
<button class="delete"></button>
<p>
{% translate "Bitte log dich ein um diese Seite sehen zu können." %}
</p>
</div>
{% endif %}
{% endif %}
{% if not user.is_authenticated %}

View File

@@ -2,12 +2,12 @@ from datetime import timedelta
from django.utils import timezone
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
deactivate_404_adoption_notices
deactivate_404_adoption_notices, mask_organization_contact_data
from fellchensammlung.tools.misc import is_404
from django.test import TestCase
from model_bakery import baker
from fellchensammlung.models import AdoptionNotice
from fellchensammlung.models import AdoptionNotice, RescueOrganization
class DeactivationTest(TestCase):
@@ -96,3 +96,21 @@ class PingTest(TestCase):
self.adoption2.refresh_from_db()
self.assertTrue(self.adoption1.is_active)
self.assertFalse(self.adoption2.is_active)
class MaskingTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.rescue1 = baker.make(RescueOrganization, email="test1@example.com", )
cls.rescue2 = baker.make(RescueOrganization, email="test-2@example.com", phone_number="0123456789", )
def test_masking(self):
mask_organization_contact_data()
self.assertEqual(RescueOrganization.objects.count(), 2)
# Ensure that the rescues are pulled from the database again, otherwise this test will fail
self.rescue1.refresh_from_db()
self.rescue2.refresh_from_db()
self.assertNotEqual(self.rescue1.phone_number, "0123456789")
self.assertEqual(self.rescue1.email, "test1-example.com@example.org")
self.assertEqual(self.rescue2.email, "test-2-example.com@example.org")

View File

@@ -85,8 +85,8 @@ class TestNotifications(TestCase):
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
def test_mark_read(self):
not1 = Notification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
not2 = Notification.objects.create(user=self.test_user_1,
not1 = Notification.objects.create(user_to_notify=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
not2 = Notification.objects.create(user_to_notify=self.test_user_1,
text="New wombat to adopt", title="🔔 New Wombat alert")
not1.mark_read()

View File

@@ -3,11 +3,12 @@ from time import sleep
from django.test import TestCase
from django.urls import reverse
from fellchensammlung.models import SearchSubscription, User, TrustLevel, AdoptionNotice, Location, SexChoicesWithAll, \
Animal, Species, AdoptionNoticeNotification, SexChoices
Animal, Species, SexChoices, Notification
from model_bakery import baker
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.tools.search import Search, notify_search_subscribers
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.tools.search import AdoptionNoticeSearch, notify_search_subscribers
class TestSearch(TestCase):
@@ -71,7 +72,7 @@ class TestSearch(TestCase):
sex=SexChoicesWithAll.ALL,
max_distance=100
)
search1 = Search()
search1 = AdoptionNoticeSearch()
search1.search_position = LocationProxy("Stuttgart").position
search1.max_distance = 100
search1.area_search = True
@@ -82,11 +83,11 @@ class TestSearch(TestCase):
self.assertEqual(search_subscription1, search1)
def test_adoption_notice_fits_search(self):
search1 = Search(search_subscription=self.subscription1)
search1 = AdoptionNoticeSearch(search_subscription=self.subscription1)
self.assertTrue(search1.adoption_notice_fits_search(self.adoption1))
self.assertFalse(search1.adoption_notice_fits_search(self.adoption2))
search2 = Search(search_subscription=self.subscription2)
search2 = AdoptionNoticeSearch(search_subscription=self.subscription2)
self.assertFalse(search2.adoption_notice_fits_search(self.adoption1))
self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))
@@ -100,5 +101,7 @@ class TestSearch(TestCase):
"""
notify_search_subscribers(self.adoption1)
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1, adoption_notice=self.adoption1).exists())
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_FOR_SEARCH_FOUND).exists())
self.assertFalse(Notification.objects.filter(user_to_notify=self.test_user2,).exists())

View File

@@ -1,7 +1,8 @@
from django.test import TestCase
from model_bakery import baker
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, AdoptionNoticeNotification
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, Notification
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.tools.notifications import notify_of_AN_to_be_checked
@@ -24,11 +25,17 @@ class TestNotifications(TestCase):
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
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())
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user0,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
self.assertFalse(Notification.objects.filter(user_to_notify=self.test_user2,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())

View File

@@ -5,8 +5,9 @@ from django.urls import reverse
from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions, Comment, CommentNotification, SearchSubscription
Animal, Subscriptions, Comment, Notification, SearchSubscription
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.views import add_adoption_notice
@@ -34,16 +35,7 @@ class AnimalAndAdoptionTest(TestCase):
species=rat,
description="Eine unglaublich süße Ratte")
def test_detail_animal(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('animal-detail', args="1"))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "Rat1")
def test_detail_animal_notice(self):
def test_detail_adoption_notice(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('adoption-notice-detail', args="1"))
@@ -101,91 +93,6 @@ class AnimalAndAdoptionTest(TestCase):
self.assertTrue(an.sexes == set("M", ))
class SearchTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
test_user0.save()
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
berlin = Location.get_location_from_string("Berlin")
adoption1.location = berlin
adoption1.save()
stuttgart = Location.get_location_from_string("Stuttgart")
adoption3.location = stuttgart
adoption3.save()
adoption1.set_active()
adoption3.set_active()
adoption2.set_unchecked()
def test_basic_view(self):
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption2")
self.assertContains(response, "TestAdoption3")
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "TestAdoption1")
self.assertContains(response, "TestAdoption3")
self.assertNotContains(response, "TestAdoption2")
def test_unauthenticated_subscribe(self):
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthenticated_unsubscribe(self):
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
"subscribe_to_search": ""})
self.assertEqual(response.status_code, 200)
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=50).exists())
def test_unsubscribe(self):
user0 = User.objects.get(username='testuser0')
self.client.login(username='testuser0', password='12345')
location = Location.get_location_from_string("München")
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
"unsubscribe_to_search": subscription.pk})
self.assertEqual(response.status_code, 200)
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=200).exists())
def test_location_search(self):
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
self.assertEqual(response.status_code, 200)
# 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
an_names = [a.name for a in response.context["adoption_notices"]]
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
class UpdateQueueTest(TestCase):
@classmethod
def setUpTestData(cls):
@@ -339,8 +246,10 @@ class AdoptionDetailTest(TestCase):
reverse('adoption-notice-detail', args=str(an1.pk)),
data={"action": "comment", "text": "Test"})
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
self.assertFalse(Notification.objects.filter(user_to_notify__username="testuser0",
notification_type=NotificationTypeChoices.NEW_COMMENT).exists())
self.assertTrue(Notification.objects.filter(user_to_notify__username="testuser1",
notification_type=NotificationTypeChoices.NEW_COMMENT).exists())
class AdoptionEditTest(TestCase):

View File

@@ -2,7 +2,8 @@ from django.test import TestCase
from django.urls import reverse
from docs.conf import language
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment, \
Location, ImportantLocation
from model_bakery import baker
@@ -38,7 +39,11 @@ class BasicViewTest(TestCase):
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
user_comment="ReportComment1")
report_comment1.save()
report_comment1.reported_broken_rules.set({rule1,})
report_comment1.reported_broken_rules.set({rule1, })
berlin = Location.get_location_from_string("Berlin")
cls.important_berlin = ImportantLocation(location=berlin, slug="berlin", name="Berlin")
cls.important_berlin.save()
def test_index_logged_in(self):
self.client.login(username='testuser0', password='12345')
@@ -60,11 +65,19 @@ class BasicViewTest(TestCase):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_about_anonymous(self):
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
def terms_of_service_logged_in(self):
response = self.client.get(reverse('terms-of-service'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def terms_of_service_anonymous(self):
response = self.client.get(reverse('terms-of-service'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_report_adoption_logged_in(self):
@@ -133,4 +146,55 @@ class BasicViewTest(TestCase):
self.assertContains(response, "ReportComment1")
self.assertContains(response, '<form action="allow" class="">')
def test_rss_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('rss'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption0")
self.assertNotContains(response, "TestAdoption5") # Should not be active, therefore not shown
def test_rss_anonymous(self):
response = self.client.get(reverse('rss'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption5")
def test_an_form_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('add-adoption'))
self.assertEqual(response.status_code, 200)
def test_an_form_anonymous(self):
response = self.client.get(reverse('add-adoption'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/vermitteln/")
def test_important_location_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('search-by-location', args=(self.important_berlin.slug,)))
self.assertEqual(response.status_code, 200)
def test_important_location_anonymous(self):
response = self.client.get(reverse('search-by-location', args=(self.important_berlin.slug,)))
self.assertEqual(response.status_code, 200)
def test_map_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('map'))
self.assertEqual(response.status_code, 200)
def test_map_anonymous(self):
response = self.client.get(reverse('map'))
self.assertEqual(response.status_code, 200)
def test_metrics_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('metrics'))
self.assertEqual(response.status_code, 200)
def test_metrics_anonymous(self):
response = self.client.get(reverse('metrics'))
self.assertEqual(response.status_code, 200)

View File

@@ -0,0 +1,113 @@
from django.test import TestCase
from django.contrib.auth.models import Permission
from django.urls import reverse
from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions, Comment, Notification, SearchSubscription
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.views import add_adoption_notice
class SearchTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Max",
last_name="BOFH",
password='12345')
test_user0.save()
test_user1 = User.objects.create_user(username='testuser1',
first_name="Moritz",
last_name="BOFH",
password='12345')
test_user1.save()
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
berlin = Location.get_location_from_string("Berlin")
adoption1.location = berlin
adoption1.save()
stuttgart = Location.get_location_from_string("Stuttgart")
adoption3.location = stuttgart
adoption3.save()
adoption1.set_active()
adoption3.set_active()
adoption2.set_unchecked()
cls.subscription1 = SearchSubscription.objects.create(owner=test_user1,
max_distance=200,
location=stuttgart,
sex="A")
def test_basic_view(self):
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption2")
self.assertContains(response, "TestAdoption3")
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "TestAdoption1")
self.assertContains(response, "TestAdoption3")
self.assertNotContains(response, "TestAdoption2")
def test_unauthenticated_subscribe(self):
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthenticated_unsubscribe(self):
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthorized_unsubscribe(self):
self.client.login(username='testuser0', password='12345')
# This should not be allowed as the subscription owner is different than the request user
response = self.client.post(reverse('search'), {"unsubscribe_to_search": self.subscription1.id})
self.assertEqual(response.status_code, 403)
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
"subscribe_to_search": ""})
self.assertEqual(response.status_code, 200)
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=50).exists())
def test_unsubscribe(self):
user0 = User.objects.get(username='testuser0')
self.client.login(username='testuser0', password='12345')
location = Location.get_location_from_string("München")
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
"unsubscribe_to_search": subscription.pk})
self.assertEqual(response.status_code, 200)
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=200).exists())
def test_location_search(self):
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
self.assertEqual(response.status_code, 200)
# 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
an_names = [a.name for a in response.context["adoption_notices"]]
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart

View File

@@ -0,0 +1,73 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.authtoken.models import Token
from model_bakery import baker
from fellchensammlung.models import AdoptionNotice, User, TrustLevel, Notification
class UserTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
cls.test_user0.trust_level = TrustLevel.ADMIN
cls.test_user0.save()
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="Mira",
last_name="Müller",
password='12345')
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user0)
notification1 = baker.make(Notification,
title="TestNotification1",
user_to_notify=cls.test_user0,
adoption_notice=adoption1)
notification2 = baker.make(Notification, title="TestNotification1", user_to_notify=cls.test_user1)
notification3 = baker.make(Notification, title="TestNotification1", user_to_notify=cls.test_user2)
token = baker.make(Token, user=cls.test_user0)
def test_detail_self(self):
self.client.login(username='testuser1', password='12345')
response = self.client.post(reverse('user-me'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Max")
def test_detail_self_via_id(self):
self.client.login(username='testuser1', password='12345')
response = self.client.post(reverse('user-detail', args=str(self.test_user1.pk)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Max")
def test_detail_admin_with_token(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('user-me'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, Token.objects.get(user=self.test_user0).key)
def test_detail_unauthenticated(self):
response = self.client.get(reverse('user-me'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/user/me/")
def test_detail_unauthorized(self):
self.client.login(username='testuser2', password='12345')
response = self.client.get(reverse('user-detail', args=str(self.test_user1.pk)))
self.assertEqual(response.status_code, 403)
def test_detail_authorized(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('user-detail', args=str(self.test_user1.pk)))
self.assertEqual(response.status_code, 200)