bug 595886, Activity Feed page

This commit is contained in:
Dave Dash 2010-11-05 14:51:28 -07:00
Родитель 719dd820af
Коммит 16396b7f1b
9 изменённых файлов: 306 добавлений и 80 удалений

Просмотреть файл

@ -10,30 +10,30 @@ _LOG = namedtuple('LOG', 'id format')
class CREATE_ADDON:
id = 1
format = _(u'{user.name} created addon {addon.name}')
format = _(u'{addon} was created.')
keep = True
class EDIT_PROPERTIES:
""" Expects: addon """
id = 2
format = _(u'{user.name} edited addon {addon.name} properties')
format = _(u'{user.name} edited addon {addon} properties')
class EDIT_DESCRIPTIONS:
id = 3
format = _(u'{user.name} edited addon {addon.name} description')
format = _(u'{user.name} edited addon {addon} description')
class EDIT_CATEGORIES:
id = 4
format = _(u'{user.name} edited categories for {addon.name}')
format = _(u'{user.name} edited categories for {addon}')
class ADD_USER_WITH_ROLE:
id = 5
format = _(u'{user.name} added {0.name} to '
'addon {addon.name} with role {1}')
'addon {addon} with role {1}')
keep = True
@ -46,18 +46,18 @@ class REMOVE_USER_WITH_ROLE:
class EDIT_CONTRIBUTIONS:
id = 7
format = _(u'{user.name} edited contributions for {addon.name}')
format = _(u'{user.name} edited contributions for {addon}')
class SET_INACTIVE:
id = 8
format = _(u'{user.name} set addon {addon.name} inactive')
format = _(u'{addon} set inactive')
keep = True
class UNSET_INACTIVE:
id = 9
format = _(u'{user.name} activated addon {addon.name}')
format = _(u'{user.name} activated addon {addon}')
keep = True
@ -97,7 +97,7 @@ class DELETE_PREVIEW:
class ADD_VERSION:
id = 16
format = _(u'{user.name} added version {0.version} to {addon}')
format = _(u'{version} added to {addon}.')
keep = True
@ -125,8 +125,7 @@ class DELETE_FILE_FROM_VERSION:
should be strings and not the object.
"""
id = 20
format = _(u'{user.name} deleted file {0} '
'from {addon} version {1}')
format = _(u'File {0} deleted from {version} of {addon}')
class APPROVE_VERSION:
@ -170,17 +169,17 @@ class REMOVE_TAG:
class ADD_TO_COLLECTION:
id = 27
format = _(u'{user.name} added addon {addon} to a collection {0.name}')
format = _(u'{addon} added to {collection}.')
class REMOVE_FROM_COLLECTION:
id = 28
forma = _(u'{user.name} removed addon {addon} from a collection {0.name}')
forma = _(u'{addon} removed from {collection}')
class ADD_REVIEW:
id = 29
format = _(u'{user.name} wrote a review about {addon}')
format = _(u'{review} for {addon} written.')
class ADD_RECOMMENDED_CATEGORY:

Просмотреть файл

@ -1,16 +1,20 @@
from copy import copy
from datetime import datetime
import json
from django.db import models
import commonware.log
from tower import ugettext_lazy as _
import amo
import amo.models
from addons.models import Addon
from users.models import UserProfile
from bandwagon.models import Collection
from reviews.models import Review
from translations.fields import TranslatedField
from users.models import UserProfile
from versions.models import Version
log = commonware.log.getLogger('devhub')
@ -79,13 +83,20 @@ class UserLog(amo.models.ModelBase):
class ActivityLogManager(amo.models.ManagerBase):
def for_addon(self, addon, limit=20, offset=0):
vals = (AddonLog.objects.filter(addon=addon)[offset:limit]
.values_list('activity_log', flat=True))
return self.filter(pk__in=list(vals))
def for_addons(self, addons):
if isinstance(addons, Addon):
addons = (addons,)
def for_user(self, user, limit=20, offset=0):
vals = (UserLog.objects.filter(user=user)[offset:limit]
vals = (AddonLog.objects.filter(addon__in=addons)
.values_list('activity_log', flat=True))
if vals:
return self.filter(pk__in=list(vals))
else:
return self.none()
def for_user(self, user):
vals = (UserLog.objects.filter(user=user)
.values_list('activity_log', flat=True))
return self.filter(pk__in=list(vals))
@ -169,22 +180,48 @@ class ActivityLog(amo.models.ModelBase):
# TODO(davedash): Support other types.
def to_string(self, type='default'):
log_type = amo.LOG_BY_ID[self.action]
arguments = self.arguments
addon = None
for arg in arguments:
if isinstance(arg, Addon) and not addon:
addon = arg
break
return log_type.format.format(*arguments, user=self.user, addon=addon)
# We need to copy arguments so we can remove elements from it
# while we loop over self.arguments.
arguments = copy(self.arguments)
addon = None
review = None
version = None
collection = None
for arg in self.arguments:
if isinstance(arg, Addon) and not addon:
addon = u'<a href="%s">%s</a>' % (arg.get_url_path(), arg.name)
arguments.remove(arg)
if isinstance(arg, Review) and not review:
review = u'<a href="%s">%s</a>' % (arg.get_url_path(),
_('Review'))
arguments.remove(arg)
if isinstance(arg, Version) and not version:
text = _('Version %s') % arg.version
version = u'<a href="%s">%s</a>' % (arg.get_url_path(), text)
arguments.remove(arg)
if isinstance(arg, Collection) and not collection:
collection = u'<a href="%s">%s</a>' % (arg.get_url_path(),
arg.name)
arguments.remove(arg)
try:
data = dict(user=self.user, addon=addon, review=review,
version=version, collection=collection)
return log_type.format.format(*arguments, **data)
except (AttributeError, KeyError, IndexError):
log.warning('%d contains garbage data' % self.id)
return 'Something magical happened.'
def __unicode__(self):
return self.to_string()
class Meta:
db_table = 'log_activity'
ordering = ('-created',)
# TODO(davedash): Remove after we finish the import.
class LegacyAddonLog(models.Model):
TYPES = [(value, key) for key, value in amo.LOG.items()]

Просмотреть файл

@ -5,7 +5,7 @@
{% endblock %}
{% block rss_feed %}
{# TODO: Add ``<link>`` to 'Recent Activity' RSS feed. #}
{# TODO(davedash): Add ``<link>`` to 'Recent Activity' RSS feed. #}
{% endblock %}
{% block content %}
@ -17,14 +17,25 @@
<section class="primary" role="main">
<div class="listing results">
<div class="results-inner">
{% if pager.object_list %}
{% for item in pager.object_list %}
<div class="item">
<p>{{ item|safe }}</p>
<p>
{% trans user=item.user|user_link, ago=item.created|timesince %}
{{ ago }} by {{ user }}
{% endtrans %}
</p>
{# TODO(cvan): Add item for each activity update. #}
<div class="item">
<p><a href="#">Version 1.3</a> of <a href="#">Test Pilot</a> added.</p>
<p>30 min ago by <a href="#">Mozilla Labs</a></p>
</div>
</div>
{% endfor %}
{% else %}
<p class="no-results">{{ _('No results found.') }}</p>
{% endif %}
</div>
{% if pager.has_other_pages() %}
<div class="listing-footer">{{ pager|paginator }}</div>
{% endif %}
</div>
</section>
@ -37,17 +48,9 @@
<h5>{{ _('Add-on') }}</h5>
<ul class="refinements">
{# TODO(cvan): Add list items for each refinement item. #}
<li class="selected">
<a href="#">{{ _('All My Add-ons') }}</a>
</li>
<li><a href="#">{{ _('Firefox Sync') }}</a></li>
<li><a href="#">{{ _('Personas Plus') }}</a></li>
<li><a href="#">{{ _('Test Pilot') }}</a></li>
{# for item in addons #}
{# include 'devhub/includes/refinement.html' #}
{# endfor #}
{% for item in addons %}
{% include 'includes/refinement.html' %}
{% endfor %}
</ul>
</div>
@ -55,18 +58,9 @@
<h5>{{ _('Activity') }}</h5>
<ul class="refinements">
{# TODO(cvan): Add list items for each refinement item. #}
<li class="selected">
<a href="#">{{ _('All Activity') }}</a>
</li>
<li><a href="#">{{ _('Add-on Updates') }}</a></li>
<li><a href="#">{{ _('Add-on Status') }}</a></li>
<li><a href="#">{{ _('User Collections') }}</a></li>
<li><a href="#">{{ _('User Reviews') }}</a></li>
{# for item in activities #}
{# include 'devhub/includes/refinement.html' #}
{# endfor #}
{% for item in activities %}
{% include 'includes/refinement.html' %}
{% endfor %}
</ul>
</div>

Двоичный файл не отображается.

Просмотреть файл

@ -17,5 +17,5 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "{user.name} added {0.name} to addon {addon.name} with role {1}"
msgstr "(ZZ) {user.name} added {0.name} to addon {addon.name} with role {1}"
msgid "{user.name} added {0.name} to addon {addon} with role {1}"
msgstr "(ZZ) {user.name} added {0.name} to addon {addon} with role {1}"

Просмотреть файл

@ -26,11 +26,11 @@ class TestActivityLog(test_utils.TestCase):
request = self.request
a = Addon.objects.get()
ActivityLog.log(request, amo.LOG['CREATE_ADDON'], a)
entries = ActivityLog.objects.for_addon(a)
entries = ActivityLog.objects.for_addons(a)
eq_(len(entries), 1)
eq_(entries[0].arguments[0], a)
eq_(unicode(entries[0]),
'Joe CamelCase created addon Delicious Bookmarks')
for x in ('Delicious Bookmarks', 'was created.'):
assert x in unicode(entries[0])
def test_json_failboat(self):
request = self.request

Просмотреть файл

@ -1,13 +1,14 @@
from decimal import Decimal
import re
import socket
from decimal import Decimal
from urllib import urlencode
from django import forms
from django.conf import settings
from django.utils import translation
import mock
from nose.tools import eq_, assert_not_equal, set_trace
from nose.tools import eq_, assert_not_equal
from pyquery import PyQuery as pq
import test_utils
@ -16,9 +17,11 @@ import paypal
from amo.urlresolvers import reverse
from addons.models import Addon, AddonUser, Charity
from applications.models import AppVersion
from bandwagon.models import Collection
from devhub.forms import ContribForm
from devhub.models import ActivityLog
from files.models import File, Platform
from reviews.models import Review
from users.models import UserProfile
from versions.models import ApplicationsVersions, License, Version
@ -53,6 +56,124 @@ class HubTest(test_utils.TestCase):
self.num_addon_clones += 1
class TestActivity(HubTest):
"""Test the activity feed."""
def setUp(self):
"""Start with one user, two add-ons."""
super(TestActivity, self).setUp()
self.clone_addon(2)
self.request = mock.Mock()
self.request.amo_user = self.user_profile
self.addon, self.addon2 = list(self.user_profile.addons.all())
def log_creates(self, num, addon=None):
if not addon:
addon = self.addon
for i in xrange(num):
ActivityLog.log(self.request, amo.LOG.CREATE_ADDON, addon)
def log_updates(self, num):
version = Version.objects.create(version='1', addon=self.addon)
for i in xrange(num):
ActivityLog.log(self.request, amo.LOG.ADD_VERSION,
(self.addon, version))
def log_status(self, num):
for i in xrange(num):
ActivityLog.log(self.request, amo.LOG.SET_INACTIVE, (self.addon))
def log_collection(self, num):
for i in xrange(num):
c = Collection(name='foo %d' % i)
ActivityLog.log(self.request, amo.LOG.ADD_TO_COLLECTION,
(self.addon, c))
def log_review(self, num):
r = Review(addon=self.addon)
for i in xrange(num):
ActivityLog.log(self.request, amo.LOG.ADD_REVIEW, (self.addon, r))
def get_pq(self, **kwargs):
url = reverse('devhub.addons.activity')
if kwargs:
url += '?' + urlencode(kwargs)
r = self.client.get(url, follow=True)
return pq(r.content)
def test_items(self):
self.log_creates(10)
doc = self.get_pq()
eq_(len(doc('.item')), 10)
def test_filter_updates(self):
self.log_creates(10)
self.log_updates(10)
doc = self.get_pq()
eq_(len(doc('.item')), 20)
doc = self.get_pq(action='updates')
eq_(len(doc('.item')), 10)
def test_filter_status(self):
self.log_creates(10)
self.log_status(5)
doc = self.get_pq()
eq_(len(doc('.item')), 15)
doc = self.get_pq(action='status')
eq_(len(doc('.item')), 5)
def test_filter_collections(self):
self.log_creates(10)
self.log_collection(3)
doc = self.get_pq()
eq_(len(doc('.item')), 13)
doc = self.get_pq(action='collections')
eq_(len(doc('.item')), 3)
def test_filter_reviews(self):
self.log_creates(10)
self.log_review(10)
doc = self.get_pq()
eq_(len(doc('.item')), 20)
doc = self.get_pq(action='reviews')
eq_(len(doc('.item')), 10)
def test_pagination(self):
self.log_review(21)
doc = self.get_pq()
# 20 items on page 1.
eq_(len(doc('.item')), 20)
# 1 item on page 2
doc = self.get_pq(page=2)
eq_(len(doc('.item')), 1)
# we have a pagination thingy
eq_(len(doc('.pagination')), 1)
assert doc('.listing-footer')
def test_no_pagination(self):
doc = self.get_pq()
assert not doc('.listing-footer')
def test_filter_addon(self):
self.log_creates(10)
self.log_creates(13, self.addon2)
# We show everything without filters
doc = self.get_pq()
eq_(len(doc('.item')), 20)
# We just show addon1
doc = self.get_pq(addon=self.addon.id)
eq_(len(doc('.item')), 10)
# we just show addon2
doc = self.get_pq(addon=self.addon2.id)
eq_(len(doc('.item')), 13)
class TestNav(HubTest):
def test_navbar(self):
@ -872,7 +993,7 @@ class TestProfile(test_utils.TestCase):
d = dict(the_reason='because', the_future='i can')
o = ActivityLog.objects
eq_(o.count(), 0)
r = self.client.post(self.url, d)
self.client.post(self.url, d)
eq_(o.filter(action=amo.LOG.EDIT_PROPERTIES.id).count(), 1)
def test_with_contributions_fields_required(self):
@ -1021,10 +1142,11 @@ class TestVersionEditFiles(TestVersionEdit):
eq_(ActivityLog.objects.count(), 1)
log = ActivityLog.objects.all()[0]
eq_(log.to_string(), u'55021 \u0627\u0644\u062a\u0637\u0628 deleted '
'file delicious_bookmarks-2.1.072-fx.xpi from '
'3615: Delicious Bookmarks '
'version 2.1.072')
eq_(log.to_string(), u'File delicious_bookmarks-2.1.072-fx.xpi '
'deleted from <a href="/en-US/firefox/addon/3615'
'/versions/2.1.072">Version 2.1.072</a> of <a '
'href="/en-US/firefox/addon/3615/">Delicious '
'Bookmarks</a>')
eq_(r.status_code, 302)
eq_(self.version.files.count(), 0)

Просмотреть файл

@ -15,12 +15,14 @@ from tower import ugettext_lazy as _lazy
from tower import ugettext as _
import amo
from amo import messages
import amo.utils
from amo import messages
from amo.helpers import urlparams
from amo.utils import MenuItem
from amo.decorators import json_view, login_required, post_required
from access import acl
import addons.forms
from addons.models import Addon, AddonUser, AddonLog
from addons import forms as addon_forms
from addons.models import Addon, AddonUser
from addons.views import BaseFilter
from devhub.models import ActivityLog
from files.models import FileUpload
@ -111,9 +113,79 @@ def ajax_compat_update(request, addon_id, addon, version_id):
compat_form=compat_form))
def _get_addons(request, addons, addon_id):
"""Create a list of ``MenuItem``s for the activity feed."""
items = []
url = request.get_full_path()
a = MenuItem()
a.selected = (not addon_id)
(a.text, a.url) = (_('All My Add-ons'), urlparams(url, page=None,
addon=None))
items.append(a)
for addon in addons:
item = MenuItem()
item.selected = (addon.id == addon_id)
(item.text, item.url) = (addon.name, urlparams(url, page=None,
addon=addon.id))
items.append(item)
return items
def _get_activities(request, action):
url = request.get_full_path()
choices = (None, 'updates', 'status', 'collections', 'reviews')
text = {None: _('All Activity'),
'updates': _('Add-on Updates'),
'status': _('Add-on Status'),
'collections': _('User Collections'),
'reviews': _('User Reviews'),
}
items = []
for c in choices:
i = MenuItem()
i.text = text[c]
i.url, i.selected = urlparams(url, page=None, action=c), (action == c)
items.append(i)
return items
def _get_filter(action):
filters = dict(updates=(amo.LOG.ADD_VERSION, amo.LOG.ADD_FILE_TO_VERSION),
status=(amo.LOG.SET_INACTIVE, amo.LOG.UNSET_INACTIVE,
amo.LOG.CHANGE_STATUS, amo.LOG.APPROVE_VERSION,),
collections=(amo.LOG.ADD_TO_COLLECTION,
amo.LOG.REMOVE_FROM_COLLECTION,),
reviews=(amo.LOG.ADD_REVIEW,))
return filters.get(action)
@login_required
def activity(request):
return jingo.render(request, 'devhub/addons/activity.html')
addons_all = request.amo_user.addons.all()
try:
addon_id = int(request.GET.get('addon'))
addons = addons_all.filter(pk=addon_id)
except (ValueError, TypeError):
addon_id = None
addons = addons_all
action = request.GET.get('action')
activities = _get_activities(request, action)
filter = _get_filter(action)
addon_items = _get_addons(request, addons_all, addon_id)
items = ActivityLog.objects.for_addons(addons)
if filter:
items = items.filter(action__in=[i.id for i in filter])
pager = amo.utils.paginate(request, items, 20)
data = dict(addons=addon_items, pager=pager, activities=activities)
return jingo.render(request, 'devhub/addons/activity.html', data)
@dev_required
@ -280,10 +352,10 @@ def upload_detail(request, uuid, format='html'):
@dev_required
def addons_section(request, addon_id, addon, section, editable=False):
models = {'basic': addons.forms.AddonFormBasic,
'details': addons.forms.AddonFormDetails,
'support': addons.forms.AddonFormSupport,
'technical': addons.forms.AddonFormTechnical}
models = {'basic': addon_forms.AddonFormBasic,
'details': addon_forms.AddonFormDetails,
'support': addon_forms.AddonFormSupport,
'technical': addon_forms.AddonFormTechnical}
if section not in models:
return http.HttpResponseNotFound()
@ -360,7 +432,8 @@ def version_list(request, addon_id, addon):
data = {'addon': addon,
'versions': versions,
'addon_status': amo.STATUS_CHOICES[addon.status] }
'addon_status': amo.STATUS_CHOICES[addon.status],
}
return jingo.render(request, 'devhub/addons/versions.html', data)

Просмотреть файл

@ -0,0 +1 @@
CREATE INDEX created_idx ON log_activity (created);