Skip to content

Commit

Permalink
Read vs Unread event
Browse files Browse the repository at this point in the history
  • Loading branch information
nsurbay committed Oct 31, 2017
1 parent 2dd461b commit 0d7f9fa
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 9 deletions.
4 changes: 4 additions & 0 deletions static/css/ponytracker.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ a .remove-label {
span.passed-due-date {
color: #FF0000;
}

.badge-unread {
background-color: #337ab7 !important;
}
9 changes: 9 additions & 0 deletions tracker/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ def process_view(self, request, view, view_args, view_kwargs):
return
project = view_kwargs.get('project')
if not project:
# count unread issues by projects
request.read_state_projects = {}
for project in request.projects.all():
request.read_state_projects[project] = project.get_unread_issues_nb(request.user)
return
try:
project = all_projects.get(name=project)
Expand All @@ -52,3 +56,8 @@ def process_view(self, request, view, view_args, view_kwargs):
request.project = project
request.archived = project.archived
request.projects = all_projects.filter(archived=request.archived)
# count unread event by issues
request.read_state_issues = {}
for issue in project.issues.all():
request.read_state_issues[issue] = issue.get_unread_event_nb(request.user)

31 changes: 31 additions & 0 deletions tracker/migrations/0011_auto_20171031_1530.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-10-31 15:30
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tracker', '0010_auto_20171026_1010'),
]

operations = [
migrations.CreateModel(
name='ReadState',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lastread', models.DateTimeField(auto_now_add=True)),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='readstates', to='tracker.Issue')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='readstates', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='readstate',
unique_together=set([('issue', 'user')]),
),
]
56 changes: 55 additions & 1 deletion tracker/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.urlresolvers import reverse
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist

from colorful.fields import RGBColorField

Expand All @@ -19,7 +20,7 @@
from accounts.models import User


__all__ = ['Project', 'Issue', 'Label', 'Milestone', 'Event']
__all__ = ['Project', 'Issue', 'Label', 'Milestone', 'ReadState', 'Event']


class Settings(models.Model):
Expand Down Expand Up @@ -92,6 +93,16 @@ def labels(self):
def milestones(self):
return Milestone.objects.filter(project=self, deleted=False)

def get_unread_issues_nb(self, user):
if not user.is_authenticated():
return 0
count = 0
for issue in self.issues.all():
if issue.have_unread_message(user):
count +=1
return count


def __str__(self):
return self.display_name

Expand Down Expand Up @@ -318,9 +329,52 @@ def remove_milestone(self, author, milestone, commit=True):
args={'milestone': milestone.name})
event.save()

def have_unread_message(self, user):
if not user.is_authenticated():
return False
try:
readstate = self.readstates.get(user=user)
except ObjectDoesNotExist:
return True
return self.events.filter(date__gt=readstate.lastread).exists()

def get_unread_event_nb(self, user):
if not user.is_authenticated():
return 0
try:
readstate = self.readstates.get(user=user)
except ObjectDoesNotExist:
return self.events.count()
return self.events.filter(date__gt=readstate.lastread).count()

def mark_as_read(self, user):
if not user.is_authenticated():
return timezone.now()
try:
readstate = self.readstates.get(user=user)
olddate = readstate.lastread
except ObjectDoesNotExist:
readstate = ReadState(issue=self, user=user)
olddate = self.opened_at
readstate.lastread = timezone.now()
readstate.save()
return olddate

def __str__(self):
return self.title

@python_2_unicode_compatible
class ReadState(models.Model):

issue = models.ForeignKey(Issue, related_name="%(class)ss")

user = models.ForeignKey(User, related_name='%(class)ss')

lastread = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ('issue', 'user')


@python_2_unicode_compatible
class Event(models.Model):
Expand Down
2 changes: 1 addition & 1 deletion tracker/templates/tracker/issue_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h1>{{ issue }} <small>#{{ issue.id }}</small></h1>
<div class="panel panel-default">

<div class="panel-heading">
<span class="badge"><span class="glyphicon glyphicon-{{ event.glyphicon }}"></span></span>
<span class="badge{% if lastread < event.date %} badge-unread{% endif %}"><span class="glyphicon glyphicon-{{ event.glyphicon }}"></span></span>
&#160;
<a href="{% same_author event.author %}">{% user_badge event.author %}</a> {{ event|safe }} {{ event.date|naturaltime }}
{% if event.code == event.DESCRIBE %}
Expand Down
8 changes: 7 additions & 1 deletion tracker/templates/tracker/issue_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
</div>
<div class="form-group">
{% if manager.resettable %}
<a href="{% issue_url reset=True %}" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-remove"> Reset filter</span></a>
<a href="{% issue_url reset=True %}" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-remove"></span> Reset filter</a>
{% endif %}
{% if manager.unread %}
<a href="{% url 'mark-read' project.name %}" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-remove"></span> Mark as read</a>
{% endif %}
</div>
</form>
Expand Down Expand Up @@ -109,6 +112,9 @@
&#160;–&#160;&#160;<span class="glyphicon glyphicon-road"></span> <a href="{% issue_url milestone=issue.milestone %}"><b>{{ issue.milestone }}</b></a>
{% endif %}
&#160;–&#160;&#160;<span><span class="badge"><span class="glyphicon glyphicon-comment"></span>&#160;{{ issue.comments.count }}</span></span>
{% if request.read_state_issues|get_item:issue > 0 %}
<span><span class="badge badge-unread"><span class="glyphicon glyphicon-bullhorn"></span>&#160;{{ request.read_state_issues|get_item:issue }}</span></span>
{% endif %}
</li>
{% endfor %}
{% else %}
Expand Down
7 changes: 6 additions & 1 deletion tracker/templates/tracker/project_list.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load tracker_tags %}

{% block content %}

Expand All @@ -22,7 +23,11 @@ <h1>
{% for project in projects %}
<div class="list-group">
<a class="list-group-item" href="{% url 'list-issue' project.name %}">
<h4>{{ project }}</h4>
<h4>{{ project }}
{% if request.read_state_projects|get_item:project > 0 %}
<span><span class="badge badge-unread"><span class="glyphicon glyphicon-bullhorn"></span>&#160;{{ request.read_state_projects|get_item:project }}</span></span>
{% endif %}
</h4>
{% if project.description %}
{{ project.description|linebreaksbr }}
{% else %}
Expand Down
4 changes: 4 additions & 0 deletions tracker/templatetags/tracker_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ def same_author(context, author):
def can_edit(context, event):
request = context['request']
return event.editable_by(request)

@register.filter
def get_item(dic, valeur):
return dic.get(valeur, None)
1 change: 1 addition & 0 deletions tracker/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
url(r'^(?P<project>[-\w]+)/delete/$', views.project_delete, name='delete-project'),
url(r'^(?P<project>[-\w]+)/subscribe/$', views.project_subscribe, name='subscribe-project'),
url(r'^(?P<project>[-\w]+)/unsubscribe/$', views.project_unsubscribe, name='unsubscribe-project'),
url(r'^(?P<project>[-\w]+)/markread/$', views.project_mark_as_read, name='mark-read'),
url(r'^(?P<project>[-\w]+)/archive/$', views.project_archive, {'archive': True}, name='archive-project'),
url(r'^(?P<project>[-\w]+)/unarchive/$', views.project_archive, {'archive': False}, name='unarchive-project'),
# Issues
Expand Down
19 changes: 15 additions & 4 deletions tracker/utils/issue_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
STATUS_VALUES = OrderedDict([
('is:open', 'Open'),
('is:close', 'Closed'),
('is:unread', 'Unread'),
('*', 'All'),
])

Expand All @@ -36,9 +37,9 @@ def shell_split(cmd):

if python_version < (3,):
cmd = cmd.encode('utf-8')

args = shlex.split(cmd)

if python_version < (3,):
args = [ arg.decode('utf-8') for arg in args ]

Expand All @@ -61,11 +62,12 @@ def get_filter_value(key, value):

class IssueManager:

def __init__(self, project, filter=None, sort=None):
def __init__(self, project, filter=None, sort=None, user=None):

self.project = project
self.filter = filter or STATUS_DEFAULT
self.sort = sort or SORT_DEFAULT
self.user = user

self.status = None
self.error = None
Expand All @@ -78,6 +80,7 @@ def __init__(self, project, filter=None, sort=None):

self._constraints = []
self._filters = []
self.unread = False

for constraint in shell_split(self.filter):

Expand Down Expand Up @@ -131,9 +134,12 @@ def handle_is(self, value):
elif value == 'close':
self.status = 'is:close'
self._filters.append(Q(closed=True))
elif value == 'unread':
self.status = 'is:unread'
self.unread = True
else:
raise ValueError("The keyword 'is' must be followed "
"by 'open' or 'close'.")
"by 'unread', 'open' or 'close'.")

def handle_label(self, value):

Expand Down Expand Up @@ -176,12 +182,14 @@ def handle_author(self, value):
raise ValueError("The user '%s' does not exist." % value)
self._filters.append(Q(author=author))


@property
def issues(self):
issues = self.project.issues
for filter in self._filters:
issues = issues.filter(filter)


if self.sort == 'newest':
issues = issues.order_by('-opened_at')
elif self.sort == 'oldest':
Expand All @@ -198,6 +206,9 @@ def issues(self):
issues = issues.annotate(last_activity=Max('events__date'))\
.order_by('-last_activity')

if self.unread:
return [ issue for issue in issues if issue.have_unread_message(self.user)]

return issues

@property
Expand Down
15 changes: 14 additions & 1 deletion tracker/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,17 @@ def project_unsubscribe(request, project):
else:
return redirect('list-issue', project.name)

@login_required
def project_mark_as_read(request, project):

for issue in project.issues.all():
issue.mark_as_read(request.user)

next = request.GET.get('next')
if next:
return redirect(next)
else:
return redirect('list-issue', project.name)

@project_perm_required('modify_project')
def project_archive(request, project, archive):
Expand Down Expand Up @@ -239,7 +250,8 @@ def issue_list(request, project):

issuemanager = IssueManager(project,
filter=request.GET.get('q'),
sort=request.GET.get('sort'))
sort=request.GET.get('sort'),
user=request.user)

issues = issuemanager.issues

Expand Down Expand Up @@ -388,6 +400,7 @@ def issue_details(request, project, issue):
'issue': issue,
'events': events,
'form': form,
'lastread' : issue.mark_as_read(request.user),
}

return render(request, 'tracker/issue_details.html', c)
Expand Down

0 comments on commit 0d7f9fa

Please sign in to comment.