Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions hub/admin/sitewide_message.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
from __future__ import annotations

from constance import config
from django.contrib import admin
from django.utils import timezone

from kobo.apps.markdownx_uploader.admin import MarkdownxModelAdminBase
from ..models import SitewideMessage


class SitewideMessageAdmin(MarkdownxModelAdminBase):

model = SitewideMessage
actions = ['require_terms_of_service_reacceptance']

def changelist_view(self, request, extra_context=None):
actions = self.get_actions(request)
# cheating: since the require_terms_of_service_reacceptance doesn't apply to
# individual SitewideMessages, always act as if all of them have been selected
# to avoid 'Items must be selected in order to perform actions on them.' error
if (
actions
and request.method == 'POST'
and 'index' in request.POST
and request.POST['action'] == 'require_terms_of_service_reacceptance'
):
data = request.POST.copy()
data['select_across'] = '1'
request.POST = data
response = self.response_action(
request, queryset=self.get_queryset(request)
)
if response:
return response
return super().changelist_view(request, extra_context)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

@admin.action(description='Require all users to reaccept the Terms of Service')
def require_terms_of_service_reacceptance(self, request, *args, **kwargs):
setattr(config, 'LAST_TOS_UPDATE', timezone.now())
self.message_user(
request, 'All users will be prompted to re-accept the Terms of Service'
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 These two user-visible strings are missing translation wrappers, violating the project rule that all text constants must be translatable. The @admin.action description and the message_user call should both use t() (which the rest of the codebase imports as from django.utils.translation import gettext as t).

Suggested change
@admin.action(description='Require all users to reaccept the Terms of Service')
def require_terms_of_service_reacceptance(self, request, *args, **kwargs):
setattr(config, 'LAST_TOS_UPDATE', timezone.now())
self.message_user(
request, 'All users will be prompted to re-accept the Terms of Service'
)
@admin.action(description=t('Require all users to reaccept the Terms of Service'))
def require_terms_of_service_reacceptance(self, request, *args, **kwargs):
setattr(config, 'LAST_TOS_UPDATE', timezone.now())
self.message_user(
request, t('All users will be prompted to re-accept the Terms of Service')
)

Rule Used: All text constants on both backend and frontend mu... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
18 changes: 18 additions & 0 deletions hub/migrations/0023_alter_sitewidemessage_slug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.13 on 2026-07-10 13:24

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('hub', '0022_delete_v1usertracker'),
]

operations = [
migrations.AlterField(
model_name='sitewidemessage',
name='slug',
field=models.CharField(max_length=50, unique=True),
),
Comment thread
greptile-apps[bot] marked this conversation as resolved.
]
2 changes: 1 addition & 1 deletion hub/models/sitewide_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class SitewideMessage(AbstractMarkdownxModel):

slug = models.CharField(max_length=50)
slug = models.CharField(max_length=50, unique=True)
body = MarkdownxField()

markdown_fields = ['body']
Expand Down
16 changes: 14 additions & 2 deletions kobo/apps/accounts/tests/test_current_user.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import re
from datetime import timedelta

import dateutil
from constance.test import override_config
from django.conf import settings
from django.core import mail
from django.urls import reverse
from django.utils import timezone
from freezegun import freeze_time
from model_bakery import baker
from rest_framework import status
from rest_framework.test import APITestCase
Expand Down Expand Up @@ -125,7 +127,7 @@ def test_validated_password_becomes_true_on_password_change(self):
def test_accepted_tos(self):
# Ensure accepted_tos is initially False
response = self.client.get(self.url)
assert response.data['accepted_tos'] == False
assert response.data['accepted_tos'] is False
assert (
'last_tos_accept_time' not in self.user.extra_details.private_data
)
Expand All @@ -146,7 +148,17 @@ def now_without_microseconds():

# Ensure accepted_tos is now True after accepting ToS
response = self.client.get(self.url)
assert response.data['accepted_tos'] == True
assert response.data['accepted_tos'] is True

# require reacceptance
with override_config(LAST_TOS_UPDATE=timezone.now()):
response = self.client.get(self.url)
assert response.data['accepted_tos'] is False
# make it a bit later and re-accept
with freeze_time(timezone.now() + timedelta(seconds=1)):
self.client.post(reverse('tos'))
response = self.client.get(self.url)
assert response.data['accepted_tos'] is True
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

@override_config(
USER_METADATA_FIELDS=[
Expand Down
1 change: 1 addition & 0 deletions kobo/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
'if field is not blank'
),
'TERMS_OF_SERVICE_URL': ('', 'URL for terms of service document'),
'LAST_TOS_UPDATE': ('', 'Date of the most recent update to the terms of service'),
'PRIVACY_POLICY_URL': ('', 'URL for privacy policy'),
'SOURCE_CODE_URL': (
'https://github.com/kobotoolbox/',
Expand Down
12 changes: 8 additions & 4 deletions kpi/serializers/current_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from zoneinfo import ZoneInfo

import constance
from constance import config
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.password_validation import validate_password
Expand Down Expand Up @@ -91,10 +92,13 @@ def get_accepted_tos(self, obj: User) -> bool:
user_extra_details = obj.extra_details
except obj.extra_details.RelatedObjectDoesNotExist:
return False
accepted_tos = (
'last_tos_accept_time' in user_extra_details.private_data.keys()
)
return accepted_tos
last_accepted = user_extra_details.private_data.get('last_tos_accept_time')
if not last_accepted:
return False
most_recent_tos_update = config.LAST_TOS_UPDATE
if not most_recent_tos_update:
return True
return last_accepted > most_recent_tos_update.strftime('%Y-%m-%dT%H:%M:%SZ')
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

@extend_schema_field(DateJoinedField)
def get_date_joined(self, obj):
Expand Down
Loading