Skip to content

Commit

Permalink
feat: Added a limit to the number of times a notice can be snoozed
Browse files Browse the repository at this point in the history
[MICROBA-1558]
  • Loading branch information
Albert (AJ) St. Aubin committed Oct 26, 2021
1 parent e171b11 commit 03df599
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ requirements/private.txt

# Virtual environments
venv/

### Editor and IDE artifacts
*~
*.swp
*.orig
.idea/
.pycharm_helpers/
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[0.9.0] - 2021-10-25
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Add in a snooze limit feature that will only allow a notice to be snoozed a number of times

[0.8.2] - 2021-10-21
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Update requirements
Expand Down
2 changes: 1 addition & 1 deletion notices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
An edx-platform plugin which manages notices that must be acknowledged.
"""

__version__ = "0.8.2"
__version__ = "0.9.0"

default_app_config = "notices.apps.NoticesConfig" # pylint: disable=invalid-name
17 changes: 17 additions & 0 deletions notices/api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""
Python API for Notice data.
"""
from django.conf import settings
from rest_framework.reverse import reverse

from notices.models import AcknowledgedNotice
from notices.selectors import get_visible_notices


Expand All @@ -24,3 +26,18 @@ def get_unacknowledged_notices_for_user(user, in_app=False, request=None):
]

return urls


def can_dismiss(user, notice):
"""
Determine whether or not the dismiss should be visible.
"""
try:
acknowledged_notice = AcknowledgedNotice.objects.get(user=user, notice=notice)
except AcknowledgedNotice.DoesNotExist:
return True

snooze_limit = settings.FEATURES.get("NOTICES_SNOOZE_COUNT_LIMIT")
if snooze_limit is not None and acknowledged_notice.snooze_count < snooze_limit:
return True
return False
18 changes: 18 additions & 0 deletions notices/migrations/0007_acknowledgednotice_snooze_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.8 on 2021-10-22 17:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('notices', '0006_auto_20211007_1346'),
]

operations = [
migrations.AddField(
model_name='acknowledgednotice',
name='snooze_count',
field=models.IntegerField(default=0),
),
]
1 change: 1 addition & 0 deletions notices/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class AcknowledgedNotice(TimeStampedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notice_acknowledgments")
notice = models.ForeignKey(Notice, on_delete=models.CASCADE, related_name="acknowledgments")
response_type = models.CharField(max_length=32, choices=RESPONSE_TYPE_CHOICES)
snooze_count = models.IntegerField(default=0)

class Meta:
"""Model metadata."""
Expand Down
8 changes: 7 additions & 1 deletion notices/rest_api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""API views for the notices app"""
from django.conf import settings
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from rest_framework import permissions
Expand Down Expand Up @@ -109,8 +110,13 @@ def post(self, request):
except Notice.DoesNotExist as exc:
raise ValidationError({"notice_id": "notice_id field does not match an existing active notice"}) from exc

AcknowledgedNotice.objects.update_or_create(
(acknowledged_notice, _) = AcknowledgedNotice.objects.update_or_create(
user=request.user, notice=notice, defaults={"response_type": acknowledgment_type}
)
snooze_limit = settings.FEATURES.get("NOTICES_SNOOZE_COUNT_LIMIT")
if snooze_limit is not None and acknowledgment_type == AcknowledgmentResponseTypes.DISMISSED:
acknowledged_notice.snooze_count = acknowledged_notice.snooze_count + 1
acknowledged_notice.save()

# Since this is just an acknowledgment API, we can just return a 204 without any response data.
return Response(status=HTTP_204_NO_CONTENT)
6 changes: 6 additions & 0 deletions notices/selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def get_visible_notices(user):
response_type=AcknowledgmentResponseTypes.DISMISSED, modified__lte=last_valid_datetime
)

snooze_limit = settings.FEATURES.get("NOTICES_SNOOZE_COUNT_LIMIT")
if snooze_limit is not None:
acknowledged_notices = acknowledged_notices.exclude(
response_type=AcknowledgmentResponseTypes.DISMISSED, snooze_count__gt=snooze_limit
)

excluded_notices = active_notices.exclude(id__in=[acked.notice.id for acked in acknowledged_notices])

return excluded_notices
1 change: 1 addition & 0 deletions notices/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ def plugin_settings(settings):
settings.FEATURES["NOTICES_DEFAULT_REDIRECT_URL"] = "http://www.example.com"
settings.FEATURES["NOTICES_FALLBACK_LANGUAGE"] = "en"
settings.FEATURES["NOTICES_SNOOZE_HOURS"] = None
settings.FEATURES["NOTICES_SNOOZE_COUNT_LIMIT"] = None
3 changes: 3 additions & 0 deletions notices/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ def plugin_settings(settings):
settings.FEATURES["NOTICES_SNOOZE_HOURS"] = settings.ENV_TOKENS.get(
"NOTICES_SNOOZE_HOURS", settings.FEATURES["NOTICES_SNOOZE_HOURS"]
)
settings.FEATURES["NOTICES_SNOOZE_COUNT_LIMIT"] = settings.ENV_TOKENS.get(
"NOTICES_SNOOZE_COUNT_LIMIT", settings.FEATURES["NOTICES_SNOOZE_COUNT_LIMIT"]
)
1 change: 1 addition & 0 deletions notices/templates/notice.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const forwardingUrl = "{{ forwarding_url }}";
const noticeId = {{ notice_id }};
const inApp = "{{ in_app }}" === "True" ? true : false;
const canDismiss = "{{ can_dismiss }}" === "True" ? true : false;
</script>
<script src="{% static 'notices/js/utils.js' %}"></script>
{{ head_content|safe }}
Expand Down
3 changes: 2 additions & 1 deletion notices/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils.translation import get_language_from_request
from django.views.generic import DetailView

from notices.api import can_dismiss
from notices.models import Notice


Expand Down Expand Up @@ -44,14 +45,14 @@ def get_context_data(self, **kwargs):
forwarding_url = settings.FEATURES["NOTICES_DEFAULT_REDIRECT_URL"]

in_app = self.request.GET.get("mobile") == "true"

context.update(
{
"head_content": self.object.head_content,
"html_content": body_content,
"forwarding_url": forwarding_url,
"notice_id": self.object.id,
"in_app": in_app,
"can_dismiss": can_dismiss(self.request.user, self.object),
}
)
return context
2 changes: 1 addition & 1 deletion test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ def root(*args):

SECRET_KEY = "insecure-secret-key"

FEATURES = {"NOTICES_SNOOZE_HOURS": None}
FEATURES = {"NOTICES_SNOOZE_HOURS": None, "NOTICES_SNOOZE_COUNT_LIMIT": None}

USE_TZ = True
1 change: 1 addition & 0 deletions test_utils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from notices.models import AcknowledgedNotice, Notice, TranslatedNoticeContent


USER_PASSWORD = "password"


Expand Down
76 changes: 76 additions & 0 deletions tests/test_selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,79 @@ def test_snoozed_notices(self):

results = get_visible_notices(self.user)
assert len(results) == 0

@override_settings(FEATURES={"NOTICES_SNOOZE_COUNT_LIMIT": 3})
def test_snooze_count_notices(self):
"""
Tests that notices can only be snoozed NOTICES_SNOOZE_COUNT_LIMIT times
"""
notices_snooze_count_limit = settings.FEATURES["NOTICES_SNOOZE_COUNT_LIMIT"]
active_notice = NoticeFactory(active=True)

AcknowledgedNoticeFactory(
user=self.user,
notice=active_notice,
response_type=AcknowledgmentResponseTypes.DISMISSED,
snooze_count=notices_snooze_count_limit + 1,
)
results = get_visible_notices(self.user)
assert len(results) == 1
print(results[0].id)
print(active_notice.id)
assert list(results) == [active_notice]

# snooze count >= limit
AcknowledgedNoticeFactory(
user=self.user, notice=active_notice, response_type=AcknowledgmentResponseTypes.DISMISSED, snooze_count=1
)

results = get_visible_notices(self.user)
assert len(results) == 0

@override_settings(FEATURES={"NOTICES_SNOOZE_HOURS": 4, "NOTICES_SNOOZE_COUNT_LIMIT": 3})
def test_snoozed_notices_with_count(self):
"""
Tests the interaction between snoozing a notice and the snooze limit.
"""
SNOOZE_HOURS = settings.FEATURES["NOTICES_SNOOZE_HOURS"]
notices_snooze_count_limit = settings.FEATURES["NOTICES_SNOOZE_COUNT_LIMIT"]

active_notice = NoticeFactory(active=True)
latest_snooze_time = datetime.datetime.now() - datetime.timedelta(hours=SNOOZE_HOURS)

# acknowledgment an hour older than the snooze limit
AcknowledgedNoticeFactory(
user=self.user,
notice=active_notice,
response_type=AcknowledgmentResponseTypes.DISMISSED,
modified=latest_snooze_time - datetime.timedelta(hours=1),
)

results = get_visible_notices(self.user)
assert len(results) == 1
assert list(results) == [active_notice]

# acknowledgment an hour newer than the snooze limit, but snooze_limit exceeded
AcknowledgedNoticeFactory(
user=self.user,
notice=active_notice,
response_type=AcknowledgmentResponseTypes.DISMISSED,
modified=latest_snooze_time + datetime.timedelta(hours=1),
snooze_count=notices_snooze_count_limit + 1,
)

results = get_visible_notices(self.user)
assert len(results) == 1
assert list(results) == [active_notice]

# acknowledgment an hour newer than the snooze limit
AcknowledgedNoticeFactory(
user=self.user,
notice=active_notice,
response_type=AcknowledgmentResponseTypes.DISMISSED,
modified=latest_snooze_time + datetime.timedelta(hours=1),
snooze_count=1,
)

results = get_visible_notices(self.user)
assert len(results) == 0

0 comments on commit 03df599

Please sign in to comment.