Skip to content
This repository was archived by the owner on Aug 29, 2026. It is now read-only.

Commit cc1487f

Browse files
committed
feat: Add option to display banner, ask for survey
1 parent fd325f9 commit cc1487f

9 files changed

Lines changed: 140 additions & 2 deletions

File tree

pdfding/e2e/test_users_e2e.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
from datetime import datetime, timedelta, timezone
12
from unittest.mock import Mock, patch
23

34
from allauth.mfa.models import Authenticator
45
from allauth.socialaccount.models import SocialAccount
6+
from django.contrib.auth.models import User
57
from django.test import override_settings
68
from django.urls import reverse
79
from helpers import PdfDingE2ENoLoginTestCase, PdfDingE2ETestCase
810
from playwright.sync_api import expect, sync_playwright
11+
from users.models import NAGGING_INTERVAL_WEEKS
912

1013

1114
class UsersE2ETestCase(PdfDingE2ETestCase):
@@ -252,6 +255,30 @@ def test_header_admin(self):
252255
self.open(reverse('pdf_overview'), p)
253256
expect(self.page.get_by_role("banner")).to_contain_text("Admin")
254257

258+
def test_nagging_banner_needs_nagging(self):
259+
self.user.profile.last_time_nagged = datetime.now(tz=timezone.utc) - timedelta(weeks=NAGGING_INTERVAL_WEEKS + 1)
260+
self.user.profile.save()
261+
262+
self.assertTrue(self.user.profile.needs_nagging)
263+
264+
with sync_playwright() as p:
265+
self.open(reverse('pdf_overview'), p)
266+
expect(self.page.locator("#nagging_banner")).to_be_visible()
267+
268+
self.page.locator("#close_nagging_banner").click()
269+
expect(self.page.locator("#nagging_banner")).not_to_be_visible()
270+
271+
changed_user = User.objects.get(id=self.user.id)
272+
self.assertFalse(changed_user.profile.needs_nagging)
273+
274+
def test_nagging_banner_needs_no_nagging(self):
275+
self.user.profile.last_time_nagged = datetime.now(tz=timezone.utc) - timedelta(weeks=NAGGING_INTERVAL_WEEKS - 1)
276+
self.user.profile.save()
277+
278+
with sync_playwright() as p:
279+
self.open(reverse('pdf_overview'), p)
280+
expect(self.page.locator("#nagging_banner")).not_to_be_visible()
281+
255282

256283
class UsersLoginE2ETestCase(PdfDingE2ENoLoginTestCase):
257284
def test_login(self):
@@ -322,7 +349,7 @@ def test_login_demo_mode(self, mock_uuid4, mock_create_demo_user):
322349

323350

324351
class EditionE2ETestCase(PdfDingE2ETestCase):
325-
@override_settings(SUPPORTER_EDITION=False, DEMO_MODE=True)
352+
@override_settings(DEMO_MODE=True)
326353
def test_demo_mode_sidebar(self):
327354
with sync_playwright() as p:
328355
self.open(reverse('home'), p)

pdfding/templates/includes/header.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{% load static %}
22
{% load i18n %}
33
{# do not remove background as it otherwise sticky will lock strange #}
4-
<header class="pt-1 md:pt-2 pb-1 md:pb-2 sticky top-0 z-40 bg-slate-50 dark:bg-slate-900">
4+
<header class="flex flex-col pt-1 md:pt-2 pb-1 md:pb-2 sticky top-0 z-40 bg-slate-50 dark:bg-slate-900">
5+
{% if request.user.is_authenticated and request.user.profile.needs_nagging %}
6+
{% include 'includes/nagging_banner.html' %}
7+
{% endif %}
58
<div class="pr-2 pl-2 md:pl-4 md:pr-4 lg:pr-6 flex items-center justify-between">
69
<a class="flex items-center gap-x-2 md:pr-4 py-1" href="/">
710
<div id="logo_div" class="flex items-center justify-center bg-primary h-8 w-8 rounded-full">
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<div id="nagging_banner" class="flex flex-row justify-between items-center
2+
bg-primary px-2 md:px-4 py-2 -mt-1! md:-mt-2! mb-2 text-slate-100">
3+
<div class="hidden md:block">
4+
</div>
5+
<div class="font-semibold">
6+
<span class="pr-1">
7+
Help us improving PdfDing by filling out the <a class="underline decoration-2" href="https://opnform.com/forms/pdfding-feedback-fdnojn">survey</a>.
8+
</span>
9+
</div>
10+
<div id="close_nagging_banner"
11+
hx-post="{% url 'update_last_time_nagged' %}"
12+
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
13+
class="cursor-pointer hover:text-white">
14+
<svg class="w-5 h-5" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
15+
<path d="M14 3.06055L9.06055 8L14 12.9385L12.9395 14L8 9.06055L3.06152 14L2.00098 12.9395L6.93848 8L2 3.06152L3.06055 2.00098L7.99902 6.93945L12.9385 2L14 3.06055Z" fill="currentColor"/>
16+
</svg>
17+
</div>
18+
</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.2.14 on 2026-06-22 01:48
2+
3+
import users.models
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('users', '0030_add_language_adjust_theme_choices'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='profile',
16+
name='last_time_nagged',
17+
field=models.DateTimeField(default=users.models.get_last_time_nagged_initial),
18+
),
19+
]

pdfding/users/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import datetime, timedelta, timezone
2+
13
from django.contrib.auth.models import User
24
from django.db import models
35
from django.db.models import QuerySet
@@ -11,6 +13,11 @@
1113
oldest_trans = _('Oldest')
1214
name_asc_trans = _('Name Asc')
1315
name_desc_trans = _('Name Desc')
16+
NAGGING_INTERVAL_WEEKS = 2
17+
18+
19+
def get_last_time_nagged_initial(): # pragma: no cover
20+
return datetime.now(tz=timezone.utc) - timedelta(weeks=5)
1421

1522

1623
class Profile(models.Model):
@@ -88,6 +95,7 @@ class LanguageChoice(models.TextChoices):
8895
dark_mode = models.CharField(choices=DarkMode.choices, max_length=6, default=DarkMode.DARK)
8996
layout = models.CharField(choices=LayoutChoice.choices, max_length=7, default=LayoutChoice.COMPACT)
9097
language = models.CharField(choices=LanguageChoice.choices, max_length=30, default=LanguageChoice.ENGLISH)
98+
last_time_nagged = models.DateTimeField(default=get_last_time_nagged_initial)
9199
pdf_inverted_mode = models.CharField(choices=EnabledChoice.choices, max_length=8, default=EnabledChoice.DISABLED)
92100
pdf_keep_screen_awake = models.CharField(
93101
choices=EnabledChoice.choices, max_length=8, default=EnabledChoice.DISABLED
@@ -242,6 +250,18 @@ def language_code(self) -> str: # pragma: no cover
242250

243251
return language_codes[self.language]
244252

253+
@property
254+
def needs_nagging(self):
255+
"""
256+
Check if a user needs to be nagged. This is used for nagging users to fill out a survey
257+
or support PdfDing through donations.
258+
"""
259+
260+
if (datetime.now(tz=timezone.utc) - self.last_time_nagged).days > NAGGING_INTERVAL_WEEKS * 7:
261+
return True
262+
else:
263+
return False
264+
245265
def has_access_to_workspace(self, workspace_id: str) -> bool:
246266
"""Check if the profile has access to the specified workspace"""
247267

pdfding/users/tests/test_models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from datetime import datetime, timedelta, timezone
2+
13
from allauth.mfa.models import Authenticator
24
from django.contrib.auth.models import User
35
from django.test import TestCase, override_settings
46
from pdf.models.collection_models import Collection
57
from pdf.models.pdf_models import Pdf
68
from pdf.models.shared_models import SharedCollection, SharedPdf
79
from pdf.services.workspace_services import create_collection, create_workspace
10+
from users.models import NAGGING_INTERVAL_WEEKS
811

912

1013
class TestProfile(TestCase):
@@ -208,3 +211,17 @@ def test_has_access_to_workspace(self):
208211

209212
self.assertTrue(profile.has_access_to_workspace(other_workspace.id))
210213
self.assertFalse(profile.has_access_to_workspace(other_user.profile.current_workspace_id))
214+
215+
def test_needs_nagging_needed(self):
216+
self.user.profile.last_time_nagged = datetime.now(tz=timezone.utc) - timedelta(
217+
weeks=(NAGGING_INTERVAL_WEEKS + 1)
218+
)
219+
self.user.profile.save()
220+
221+
self.assertEqual(self.user.profile.needs_nagging, True)
222+
223+
def test_needs_nagging_not_needed(self):
224+
self.user.profile.last_time_nagged = datetime.now(tz=timezone.utc) - timedelta(weeks=NAGGING_INTERVAL_WEEKS - 1)
225+
self.user.profile.save()
226+
227+
self.assertEqual(self.user.profile.needs_nagging, False)

pdfding/users/tests/test_views.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime, timezone
12
from unittest.mock import Mock, patch
23

34
from allauth.socialaccount.models import SocialAccount
@@ -282,6 +283,20 @@ def test_open_collapse_tags_post(self):
282283
changed_user = User.objects.get(id=self.user.id)
283284
self.assertFalse(changed_user.profile.tags_open)
284285

286+
def test_update_last_time_nagged_no_htmx(self):
287+
response = self.client.post(reverse('update_last_time_nagged'))
288+
289+
self.assertRedirects(response, reverse('pdf_overview'), status_code=302)
290+
291+
def test_a_update_last_time_nagged(self):
292+
self.assertTrue((datetime.now(timezone.utc) - self.user.profile.last_time_nagged).total_seconds() > 1000)
293+
294+
headers = {'HTTP_HX-Request': 'true'}
295+
296+
self.client.post(reverse('update_last_time_nagged'), **headers)
297+
changed_user = User.objects.get(id=self.user.id)
298+
self.assertTrue((datetime.now(timezone.utc) - changed_user.profile.last_time_nagged).total_seconds() < 0.1)
299+
285300
def test_change_sorting_post_shared_pdf(self):
286301
self.assertEqual(self.user.profile.shared_pdf_sorting, Profile.SharedPdfSortingChoice.NEWEST)
287302

pdfding/users/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
path('change_workspace/<workspace_id>', views.ChangeWorkspace.as_view(), name="change_workspace"),
1616
path('change_collection/<collection_id>', views.ChangeCollection.as_view(), name="change_collection"),
1717
path('open_collapse_tags', views.OpenCollapseTags.as_view(), name="open_collapse_tags"),
18+
path('update_last_time_nagged', views.UpdateLastTimeNagged.as_view(), name="update_last_time_nagged"),
1819
path('signatures', views.Signatures.as_view(), name="signatures"),
1920
]

pdfding/users/views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from datetime import datetime, timezone
23
from random import randint
34
from uuid import uuid4
45

@@ -252,6 +253,23 @@ def post(self, request: HttpRequest):
252253
return redirect('account_settings')
253254

254255

256+
class UpdateLastTimeNagged(View):
257+
"""View for updating the last time a user was nagged."""
258+
259+
def post(self, request: HttpRequest):
260+
"""Update the last time a user was nagged with the current datetime."""
261+
262+
if request.htmx: # type: ignore
263+
user_profile = request.user.profile # type: ignore
264+
user_profile.last_time_nagged = datetime.now(tz=timezone.utc)
265+
266+
user_profile.save()
267+
268+
return HttpResponseClientRefresh()
269+
270+
return redirect('pdf_overview')
271+
272+
255273
class Signatures(View):
256274
"""View for gettings and setting signatures"""
257275

0 commit comments

Comments
 (0)