Skip to content

Commit

Permalink
Merge pull request #2892 from HyphaApp/feature/gh-2812-in-app-notific…
Browse files Browse the repository at this point in the history
…ations

Implementing in-app notification
  • Loading branch information
frjo authored Jul 15, 2022
2 parents 623a9c0 + 6c5e46f commit df2850f
Show file tree
Hide file tree
Showing 15 changed files with 276 additions and 1 deletion.
9 changes: 9 additions & 0 deletions hypha/apply/activity/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .models import Activity


def notification_context(request):
context_data = dict()
if hasattr(request, 'user'):
if request.user.is_authenticated and request.user.is_apply_staff:
context_data['latest_notifications'] = Activity.objects.latest().order_by('-timestamp')[:5]
return context_data
43 changes: 43 additions & 0 deletions hypha/apply/activity/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from datetime import timedelta

import django_filters
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_filters.filters import DateRangeFilter, _truncate

from .models import Activity


class NotificationFilter(django_filters.FilterSet):
timestamp_choices = [
('today', _('Today')),
('yesterday', _('Yesterday')),
('week', _('Past 7 days')),
('month', _('This month'))
]
timestamp_filters = {
'today': lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month,
'%s__day' % name: now().day
}),
'yesterday': lambda qs, name: qs.filter(**{
'%s__year' % name: (now() - timedelta(days=1)).year,
'%s__month' % name: (now() - timedelta(days=1)).month,
'%s__day' % name: (now() - timedelta(days=1)).day,
}),
'week': lambda qs, name: qs.filter(**{
'%s__gte' % name: _truncate(now() - timedelta(days=7)),
'%s__lt' % name: _truncate(now() + timedelta(days=1)),
}),
'month': lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month
})
}

date = DateRangeFilter(field_name='timestamp', choices=timestamp_choices, filters=timestamp_filters)

class Meta:
model = Activity
fields = {}
4 changes: 4 additions & 0 deletions hypha/apply/activity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.db import models
from django.db.models import Case, Value, When
from django.db.models.functions import Concat
from django.utils import timezone

from .options import MESSAGES

Expand Down Expand Up @@ -54,6 +55,9 @@ def comments(self):
def actions(self):
return self.filter(type=ACTION)

def latest(self):
return self.filter(timestamp__gte=(timezone.now() - timezone.timedelta(days=30)))


class ActivityBaseManager(models.Manager):
def create(self, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load i18n activity_tags bleach_tags markdown_tags submission_tags apply_tags %}

<div class="notifications notifications--dropdown">
<a href="#" class="button button--contains-icons notifications__bell" aria-label="{% trans "Notifications" %}" aria-haspopup="activity" aria-expanded="false" role="button">
<svg class="icon"><use xlink:href="#bell-icon"></use></svg>
</a>
<div class="notifications__content zeta hidden" role="activity">
<h5>Notifications</h5>
{% for activity in latest_notifications %}
<p class="notifications__item">
<strong>{{ activity.source_content_type.name|source_type }} </strong>
<a href="{{ activity.source.get_absolute_url }}{% ifequal activity.type 'comment' %}#communications{% endifequal %}">{{ activity.source.title|capfirst|truncatechars:15 }}</a>
: {{ activity.user.get_full_name }} {% ifequal activity.type 'comment' %}{% trans "made a comment" %}{% else %} {{ activity|display_for:request.user }}{% endifequal %}
{% if activity.related_object %}<a href="{{ activity.related_object.get_absolute_url }}">{{ activity.related_object|model_verbose_name }}</a>{% endif %}
</p>
{% endfor %}
<p class="notifications__more"><a href="{% url "activity:notifications" %}">Show All</a></p>
</div>
</div>
72 changes: 72 additions & 0 deletions hypha/apply/activity/templates/activity/notifications.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{% extends "base-apply.html" %}
{% load i18n static activity_tags apply_tags bleach_tags markdown_tags submission_tags %}

{% block content %}
<div class="admin-bar">
<div class="admin-bar__inner">
<div class="admin-bar__inner--with-button">
<h1 class="gamma heading heading--no-margin heading--bold">{% trans "Notifications" %}</h1>
<form class="form notifications__filters" method="get">
{{ filter.form }}
<button class="button button--primary" type="submit" value="Filter">{% trans "Filter" %}</button>
</form>
</div>

<div class="tabs js-tabs">
<div class="tabs__container">
<a class="tab__item" href="#comments" data-tab="tab-1">
{% trans "Communications" %}
</a>

<a class="tab__item" href="#actions" data-tab="tab-2">
{% trans "Activity Feed" %}
</a>
</div>
</div>
</div>
</div>

<div class="wrapper wrapper--large wrapper--tabs js-tabs-content">
{# Tab 1 #}
<div class="tabs__content" id="tab-1">
{% for activity in object_list %}
{% if activity.type == 'comment' %}
<div class="feed__item feed__item--{{ activity.type }}">
<div class="feed__pre-content">
<p class="feed__label feed__label--{{ activity.source_content_type.name|source_type|lower }}">{{ activity.source_content_type.name|source_type }}</p>
</div>
<div class="feed__content js-feed-content">
<div class="feed__meta js-feed-meta">
<p class="feed__meta-item"><a href="{{ activity.source.get_absolute_url }}#communications">{{ activity.source.title|capfirst }}</a>
: {{ activity.user.get_full_name }} {% trans "made a comment" %} – {{ activity.timestamp|date:"SHORT_DATE_FORMAT" }}</p>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{# Tab 2 #}
<div class="tabs__content" id="tab-2">
{% for activity in object_list %}
{% if activity.type == 'action' %}
<div class="feed__item feed__item--{{ activity.type }}">
<div class="feed__pre-content">
<p class="feed__label feed__label--{{ activity.source_content_type.name|source_type|lower }}">{{ activity.source_content_type.name|source_type }}</p>
</div>
<div class="feed__content js-feed-content">
<div class="feed__meta js-feed-meta">
<p class="feed__meta-item"><a href="{{ activity.source.get_absolute_url }}">{{ activity.source.title|capfirst }}</a>
: {{ activity.user.get_full_name }} – {{ activity.timestamp|date:"SHORT_DATE_FORMAT" }} – {{ activity|display_for:request.user }} {% if activity.related_object %}<a href="{{ activity.related_object.get_absolute_url }}" class="feed__related-item">{{ activity.related_object|model_verbose_name }} <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg></a>{% endif %}</p>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>

{% endblock %}

{% block extra_js %}
<script src="{% static 'js/apply/tabs.js' %}"></script>
{% endblock %}
7 changes: 7 additions & 0 deletions hypha/apply/activity/templatetags/activity_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ def display_for(activity, user):
def visibility_options(activity, user):
choices = activity.visibility_choices_for(user)
return json.dumps(choices)


@register.filter
def source_type(value):
if value and "submission" in value:
return "Submission"
return str(value).capitalize()
3 changes: 3 additions & 0 deletions hypha/apply/activity/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.urls import include, path

from .views import NotificationsView

app_name = 'activity'


urlpatterns = [
path('anymail/', include('anymail.urls')),
path('notifications/', NotificationsView.as_view(), name='notifications')
]
23 changes: 22 additions & 1 deletion hypha/apply/activity/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.utils import timezone
from django.views.generic import CreateView
from django.utils.decorators import method_decorator
from django.views.generic import CreateView, ListView

from hypha.apply.users.decorators import staff_required
from hypha.apply.utils.views import DelegatedViewMixin

from .filters import NotificationFilter
from .forms import CommentForm
from .messaging import MESSAGES, messenger
from .models import COMMENT, Activity
Expand Down Expand Up @@ -57,3 +60,21 @@ def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.pop('instance')
return kwargs


@method_decorator(staff_required, name='dispatch')
class NotificationsView(ListView):
model = Activity
template_name = 'activity/notifications.html'
filterset_class = NotificationFilter

def get_queryset(self):
# List only last 30 days' activities
queryset = Activity.objects.latest()
self.filterset = self.filterset_class(self.request.GET, queryset=queryset)
return self.filterset.qs.distinct().order_by('-timestamp')

def get_context_data(self, *, object_list=None, **kwargs):
context = super(NotificationsView, self).get_context_data()
context['filter'] = self.filterset
return context
1 change: 1 addition & 0 deletions hypha/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
'social_django.context_processors.login_redirect',
'hypha.apply.projects.context_processors.projects_enabled',
'hypha.cookieconsent.context_processors.cookies_accepted',
'hypha.apply.activity.context_processors.notification_context',
],
},
},
Expand Down
21 changes: 21 additions & 0 deletions hypha/static_src/src/javascript/apply/notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
(function () {

'use strict';

// Open/close dropdown when users clicks the bell.
document.querySelector('.notifications__bell').addEventListener('click', function () {
document.querySelector('.notifications__content').classList.toggle('hidden');
});

// Close the dropdown menu if the user clicks outside of it.
window.onclick = function (event) {
if (!event.target.matches('.notifications--dropdown, .notifications--dropdown *')) {
const dropdown = document.querySelector('.notifications__content');
if (!dropdown.classList.contains('hidden')) {
dropdown.classList.add('hidden');
}
}
};


})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.notifications {
&--dropdown {
position: relative;
display: inline-block;
z-index: 200;
}

&__bell {
padding: 7px 12px;
cursor: pointer;
}

&__content {
position: absolute;
right: 1em;
padding: 1em;
margin-top: .5em;
background-color: $color--light-grey;
min-width: 400px;
box-shadow: 2px 2px 6px 1px $color--dark-grey;
}

&__item {
padding-bottom: 1em;
border-bottom: 1px solid $color--dark-grey;
}

&__more {
text-align: center;
font-weight: $weight--semibold;
}

&__filters {
display: flex;
align-items: center;
padding: 4px;
justify-content: space-between;

label {
font-weight: $weight--semibold;
padding-right: 1em;
}

select {
padding-right: 1em;
}

.form {
&__select {
margin-right: 1em;
}
}
}
}
10 changes: 10 additions & 0 deletions hypha/static_src/src/sass/apply/components/_feed.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@
background-color: $color--mint;
}

&--submission {
background-color: $color--green;
}

&--project {
background-color: $color--mint;
}

&--mobile {
display: block;
margin-right: 10px;
Expand Down Expand Up @@ -152,6 +160,8 @@
}

&__related-item {
white-space: nowrap;

svg {
width: 10px;
height: 14px;
Expand Down
1 change: 1 addition & 0 deletions hypha/static_src/src/sass/apply/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
@import 'components/reminder-sidebar';
@import 'components/two-factor';
@import 'components/determination';
@import 'components/activity-notifications';

// Layout
@import 'layout/header';
Expand Down
6 changes: 6 additions & 0 deletions hypha/templates/base-apply.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/js.cookie.min.js' %}"></script>
<script src="{% static 'js/main-top.js' %}"></script>
{% if latest_notifications %}
<script defer src="{% static 'js/apply/notifications.js' %}"></script>
{% endif %}
{% if COOKIES_ACCEPTED and MATOMO_URL and MATOMO_SITEID %}
{# we are only expecting strings, so make sure we escape the values #}
<script>
Expand Down Expand Up @@ -104,6 +107,9 @@
</section>

<div class="header__button-container">
{% if latest_notifications %}
{% include "activity/include/notifications_dropdown.html" %}
{% endif %}
<a href="{% url 'users:account' %}" class="button button--transparent button--narrow button--contains-icons">
<svg class="icon icon--person"><use xlink:href="#person-icon"></use></svg>
{{ request.user }}
Expand Down
4 changes: 4 additions & 0 deletions hypha/templates/includes/sprites.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@
<path d="M17 9.81V16h-3.644v-5.776c0-1.45-.527-2.441-1.846-2.441-1.006 0-1.605.667-1.87 1.313-.095.23-.12.552-.12.875V16H5.875s.05-9.782 0-10.796H9.52v1.53l-.024.035h.024v-.035c.484-.734 1.349-1.783 3.284-1.783C15.202 4.95 17 6.494 17 9.81zM2.062 0C.816 0 0 .806 0 1.865 0 2.9.792 3.73 2.014 3.73h.024c1.272 0 2.062-.83 2.062-1.866C4.076.805 3.31 0 2.062 0zM.216 16H3.86V5.204H.216V16z" fill-rule="nonzero" />
</symbol>

<symbol id="bell-icon" viewBox="0 0 16 16">
<path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zm.995-14.901a1 1 0 1 0-1.99 0A5.002 5.002 0 0 0 3 6c0 1.098-.5 6-2 7h14c-1.5-1-2-5.902-2-7 0-2.42-1.72-4.44-4.005-4.901z"/>
</symbol>

<symbol id="arrow-head-pixels--transparent" viewBox="0 0 50 75">
<g fill="#25AAE1" fill-rule="evenodd">
<path opacity=".2" d="M0 50h25v25H0z" />
Expand Down

0 comments on commit df2850f

Please sign in to comment.