Permalink
Browse files

paginated timelines, and abstracted out logic

This adds:
 * object editor.models.Timeline, representing a stream of TimelineItems, which is effectively a wrapper around the TimelineItem object manager, with some filters added
 * a `timeline` template tag, to render a given timeline queryset. Items which can't be seen by the user are auotmatically filtered out. The results are paginated.

This adds a dependency on the package django-el-pagination.
  • Loading branch information...
1 parent b163654 commit 081431e1b2f0f19b477bc1bf9a171c933ee4999e @christianp christianp committed Oct 10, 2016
@@ -18,10 +18,9 @@ <h1 class="big-name">
<div class="container-fluid">
<div class="row">
<div class="col-sm-3">
- <div class="thumbnail thumbnail-no-border"><a href="{% url 'edit_profile' %}" title="Change your profile picture">{% user_thumbnail view_user 150 %}</a></div>
+ <div class="thumbnail thumbnail-no-border">{% if view_user == user %}<a href="{% url 'edit_profile' %}" title="Change your profile picture">{% user_thumbnail view_user 150 %}</a>{% else %}{% user_thumbnail view_user 150 %}{% endif %}</div>
<div class="nav nav-pills nav-stacked">
<li class="{% if profile_page == "bio" %}active{% endif %}"><a href="{% url 'view_profile' object.pk %}"><span class="glyphicon glyphicon-user"></span> Profile</a></li>
- <li class="{% if profile_page == "activity" %}active{% endif %}"><a href="{% url 'profile_activity' object.pk %}"><span class="glyphicon glyphicon-dashboard"></span> Activity</a></li>
<li class="{% if profile_page == "projects" %}active{% endif %}"><a href="{% url 'profile_projects' object.pk %}"><span class="glyphicon glyphicon-briefcase"></span> Projects</a></li>
<li class="{% if profile_page == "themes" %}active{% endif %}"><a href="{% url 'profile_themes' object.pk %}"><span class="glyphicon glyphicon-sunglasses"></span> Themes</a></li>
<li class="{% if profile_page == "extensions" %}active{% endif %}"><a href="{% url 'profile_extensions' object.pk %}"><span class="glyphicon glyphicon-wrench"></span> Extensions</a></li>
@@ -1,12 +0,0 @@
-{% extends "profile/base.html" %}
-{% load user_link %}
-
-{% block profile_content %}
-<div class="timeline">
- {% with include_object_link=True %}
- {% for item in view_user.userprofile.public_timeline|slice:":20" %}
- {% include item.object.timelineitem_template %}
- {% endfor %}
- {% endwith %}
-</div>
-{% endblock profile_content %}
@@ -1,6 +1,7 @@
{% extends "profile/base.html" %}
{% load escape_html from sanitizer %}
{% load user_link %}
+{% load timeline %}
{% block profile_content %}
{% with object.userprofile as profile %}
@@ -25,5 +26,15 @@
<p><a href="{% url 'profile_editoritem_search' view_user.pk %}" class="btn btn-link btn-lg"><span class="glyphicon glyphicon-search"></span> Browse {{view_user.first_name}}'s content</a></p>
+<hr>
+
+<h2 class="timeline-header"><span class="glyphicon glyphicon-dashboard"></span> {{user.first_name}}'s activity</h2>
+<div class="timeline">
+ {% with include_object_link=True %}
+ {% timeline profile.public_timeline %}
+ {% endwith %}
+</div>
+
+
{% endwith %}
{% endblock profile_content %}
View
@@ -55,9 +55,6 @@
url(r'^accounts/profile/(?P<pk>\d+)/items$',
accounts.views.UserEditorItemSearchView.as_view(),
name='profile_editoritem_search'),
- url(r'^accounts/profile/(?P<pk>\d+)/activity$',
- accounts.views.UserTimelineView.as_view(),
- name='profile_activity'),
url(r'^accounts/profile/(?P<pk>\d+)/projects$',
accounts.views.UserProjectsView.as_view(),
name='profile_projects'),
View
@@ -11,7 +11,7 @@
from accounts.forms import UserProfileForm,ChangePasswordForm
from editor.models import NewQuestion, NewExam
import editor.models
-from editor.views import editoritem
+from editor.views import editoritem,timeline
from zipfile import ZipFile
from cStringIO import StringIO
from django.contrib.sites.models import Site
@@ -113,10 +113,6 @@ def get_context_data(self,*args,**kwargs):
context['profile_page'] = self.profile_page
return context
-class UserTimelineView(UserProfileView):
- template_name = 'profile/timeline.html'
- profile_page = 'activity'
-
class UserProjectsView(UserProfileView):
template_name = 'profile/projects.html'
profile_page = 'projects'
View
@@ -33,6 +33,7 @@
from django.utils.translation import ugettext_lazy as _
from django.template.loader import get_template
from django.core.mail import send_mail
+from django.core.paginator import Paginator
import reversion
@@ -63,7 +64,7 @@ def has_access(self,user,accept_levels):
def can_be_viewed_by(self,user):
accept_levels = ('view','edit')
try:
- if self.public_access in accept_levels:
+ if self.published and self.public_access in accept_levels:
return True
except AttributeError:
pass
@@ -117,6 +118,9 @@ def can_be_deleted_by(self,user):
pass
return user==self.user
+ def can_be_viewed_by(self,user):
+ raise NotImplementedError
+
def timeline_object(self):
try:
return self.object
@@ -187,6 +191,9 @@ class ProjectAccess(models.Model,TimelineMixin):
def can_be_deleted_by(self,user):
return self.project.can_be_edited_by(user)
+ def can_be_viewed_by(self,user):
+ return self.project.can_be_viewed_by(user)
+
def timeline_object(self):
return self.project
@@ -454,6 +461,9 @@ class Access(models.Model,TimelineMixin):
timelineitems = GenericRelation('TimelineItem',related_query_name='item_accesses',content_type_field='object_content_type',object_id_field='object_id')
timelineitem_template = 'timeline/access.html'
+ def can_be_viewed_by(self,user):
+ return self.item.can_be_viewed_by(user)
+
def can_be_deleted_by(self,user):
return self.item.can_be_deleted_by(user)
@@ -749,6 +759,9 @@ def can_be_merged_by(self,user):
def can_be_deleted_by(self,user):
return user==self.owner or self.destination.can_be_edited_by(user)
+ def can_be_viewed_by(self,user):
+ return self.source.can_be_viewed_by(user) and self.destination.can_be_viewed_by(user)
+
def clean(self):
if self.source==self.destination:
raise ValidationError({'source': "Source and destination are the same."})
@@ -772,9 +785,35 @@ def close(self,user):
self.open = False
self.closed_by = user
+class Timeline(object):
+ def __init__(self,items,viewing_user):
+ self.viewing_user = viewing_user
+ items = items.prefetch_related('object')
+
+ nonsticky_broadcasts = SiteBroadcast.objects.visible_now().exclude(sticky=True)
+ nonsticky_broadcast_timelineitems = TimelineItem.objects.filter(object_content_type=ContentType.objects.get_for_model(SiteBroadcast),object_id__in=nonsticky_broadcasts)
+
+ filtered_items = items.filter(editoritems__published=True) | nonsticky_broadcast_timelineitems
+ if not self.viewing_user.is_anonymous():
+ projects = self.viewing_user.own_projects.all() | Project.objects.filter(projectaccess__in=self.viewing_user.project_memberships.all()) | Project.objects.filter(watching_non_members=self.viewing_user)
+ items_for_user = TimelineItem.objects.filter(
+ Q(editoritems__in=self.viewing_user.watched_items.all()) |
+ Q(editoritems__project__in=projects) |
+ Q(projects__in=projects)
+ )
+
+ filtered_items = filtered_items | items_for_user
+ filtered_items = filtered_items.exclude(hidden_by=self.viewing_user)
+
+ self.filtered_items = filtered_items
+
+ def __getitem__(self,index):
+ return self.filtered_items.__getitem__(index)
+
class TimelineItemManager(models.Manager):
def visible_to(self,user):
- return self.exclude(hidden_by=user)
+ objects = self.exclude(hidden_by=user)
+ return objects
class TimelineItem(models.Model):
objects = TimelineItemManager()
@@ -795,12 +834,18 @@ class TimelineItem(models.Model):
date = models.DateTimeField(auto_now_add=True)
+ def __unicode__(self):
+ return '{}: {}'.format(self.date,str(self.object))
+
def can_be_deleted_by(self,user):
try:
return self.object.can_be_deleted_by(user)
except AttributeError:
return False
+ def can_be_viewed_by(self,user):
+ return self.user==user or self.object.can_be_viewed_by(user)
+
class Meta:
unique_together = (('object_id','object_content_type'),)
ordering = ('-date',)
@@ -829,6 +874,9 @@ class SiteBroadcast(models.Model,TimelineMixin):
def can_be_deleted_by(self,user):
return False
+ def can_be_viewed_by(self,user):
+ return True
+
def timeline_object(self):
return None
@@ -847,6 +895,9 @@ class NewStampOfApproval(models.Model,TimelineMixin):
def __unicode__(self):
return '{} said "{}"'.format(self.user.username,self.get_status_display())
+ def can_be_viewed_by(self,user):
+ return self.object.can_be_viewed_by(user)
+
class Comment(models.Model,TimelineMixin):
object_content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
@@ -861,6 +912,9 @@ class Comment(models.Model,TimelineMixin):
def __unicode__(self):
return 'Comment by {}: "{}"'.format(self.user.get_full_name(), str(self.object), self.text[:47]+'...' if len(self.text)>50 else self.text)
+ def can_be_viewed_by(self,user):
+ return self.object.can_be_viewed_by(user)
+
class RestorePoint(models.Model,TimelineMixin):
object = models.ForeignKey(EditorItem,related_name='restore_points')
@@ -875,6 +929,9 @@ class RestorePoint(models.Model,TimelineMixin):
def __unicode__(self):
return 'Restore point set by {}: "{}"'.format(self.user.get_full_name(), str(self.object), self.description[:47]+'...' if len(self.description)>50 else self.description)
+ def can_be_viewed_by(self,user):
+ return self.object.can_be_viewed_by(user)
+
ITEM_CHANGED_VERBS = [('created','created')]
class ItemChangedTimelineItem(models.Model,TimelineMixin):
object = models.ForeignKey(EditorItem)
@@ -883,6 +940,9 @@ class ItemChangedTimelineItem(models.Model,TimelineMixin):
timelineitems = GenericRelation(TimelineItem,related_query_name='item_changes',content_type_field='object_content_type',object_id_field='object_id')
timelineitem_template = 'timeline/change.html'
+
+ def can_be_viewed_by(self,user):
+ return self.object.can_be_viewed_by(user)
def can_be_deleted_by(self,user):
return False
@@ -1604,3 +1664,6 @@ class StampOfApproval(models.Model,TimelineMixin):
def __unicode__(self):
return '{} as "{}"'.format(self.user.username,self.object.name,self.get_status_display(),self.date)
+ def can_be_viewed_by(self,user):
+ return self.object.can_be_viewed_by(user)
+
@@ -33,15 +33,23 @@ body:not(.loaded) .loaded-content {
display: block;
}
-.timeline-item {
- border-bottom: 1px solid #eee;
- padding-bottom: 1em;
+.timeline-header {
+ margin-bottom: 3rem;
+}
+
+.timeline-item + .timeline-item {
+ border-top: 1px solid #eee;
+ padding-top: 1em;
}
.timeline-item.less-important .media-heading {
font-size: 1em;
color: #888;
}
+.btn.timeline-more {
+ margin-top: 1em;
+}
+
.user-thumbnail .glyphicon {
color: black !important;
}
@@ -62,11 +62,9 @@
{% include item.object.timelineitem_template %}
{% endwith %}
{% endfor %}
- {% with include_object_link=True timeline=request.user.userprofile.all_timeline|visible_to:request.user %}
+ {% with include_object_link=True timeline=request.user.userprofile.all_timeline %}
{% if timeline.exists %}
- {% for item in timeline|slice:":20" %}
- {% include item.object.timelineitem_template %}
- {% endfor %}
+ {% timeline timeline per_page=20 %}
{% else %}
{% with include_object_link=False %}
{% include "timeline/welcome.html" %}
@@ -41,11 +41,9 @@
</div>
{% endif %}
<div class="timeline">
- {% with include_object_link=True timeline=project.all_timeline|visible_to:request.user %}
+ {% with include_object_link=True timeline=project.all_timeline %}
{% if timeline.exists %}
- {% for item in timeline|slice:":20" %}
- {% include item.object.timelineitem_template %}
- {% endfor %}
+ {% timeline timeline per_page=20 %}
{% else %}
<p class="nothing-here">No activity yet!</p>
{% endif %}
@@ -0,0 +1,7 @@
+{% load el_pagination_tags %}
+
+{% lazy_paginate timeline_items_per_page timeline as items %}
+{% for item in items %}
+ {% include item.object.timelineitem_template %}
+{% endfor %}
+{% show_more "Earlier activity →" "" "btn btn-info btn-lg timeline-more" %}
@@ -1,6 +1,7 @@
from django.core.serializers import serialize
import json
from django.template import Library
+from editor.models import Timeline
register = Library()
@@ -14,3 +15,13 @@ def visible_to(items,user):
return items
else:
return items.exclude(hidden_by=user)
+
+@register.inclusion_tag('timeline/timeline.html',takes_context=True)
+def timeline(context,items,**kwargs):
+ return {
+ 'timeline': Timeline(items,context['user']),
+ 'request': context.get('request'),
+ 'include_object_link': context.get('include_object_link',False),
+ 'timeline_items_per_page': kwargs.get('per_page',10),
+ }
+
@@ -235,27 +235,6 @@ def get(self, request, *args, **kwargs):
self.version.revision.revert()
return redirect(reverse('question_edit', args=(self.question.pk,self.question.editoritem.slug)))
-
-class JSONSearchView(editor.views.editoritem.SearchView):
-
- """Search questions."""
-
- def render_to_response(self, context, **response_kwargs):
- if self.request.is_ajax():
- return HttpResponse(json.dumps({'object_list':context['object_list'],'page':context['page'],'id':context['id']}),
- content_type='application/json',
- **response_kwargs)
- raise Http404
-
- def get_queryset(self):
- questions = super(JSONSearchView,self).get_queryset()
- return [q.summary() for q in questions]
-
- def get_context_data(self, **kwargs):
- context = ListView.get_context_data(self,**kwargs)
- context['page'] = self.request.GET.get('page',1)
- context['id'] = self.request.GET.get('id',None)
- return context
class ShareLinkView(editor.views.generic.ShareLinkView):
permanent = False
@@ -17,3 +17,4 @@ django-reversion==1.8.7,<1.9
django-notifications-hq==1.0.*
django-thumbs==0.4
sqlparse==0.1.16
+django-el-pagination==3.0.1

0 comments on commit 081431e

Please sign in to comment.