sharing for collections (bug 586444)

This commit is contained in:
Jeff Balogh 2010-08-24 15:22:15 -07:00
Родитель a10c20bd06
Коммит ec4c45d614
19 изменённых файлов: 183 добавлений и 47 удалений

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

@ -15,8 +15,9 @@ import amo.models
from amo.fields import DecimalCharField
from amo.utils import urlparams, sorted_groupby, JSONEncoder
from amo.urlresolvers import reverse
from cake.urlresolvers import remora_url
from reviews.models import Review
from stats.models import Contribution as ContributionStats, ShareCountTotal
from stats.models import Contribution as ContributionStats, AddonShareCountTotal
from translations.fields import (TranslatedField, PurifiedField,
LinkifiedField, translations_with_fallback)
from users.models import UserProfile, PersonaAuthor
@ -227,6 +228,9 @@ class Addon(amo.models.ModelBase):
"""The url for this add-on's AddonType."""
return AddonType(self.type).get_url_path()
def share_url(self):
return remora_url('/addon/share/%s' % self.id)
@amo.cached_property(writable=True)
def listed_authors(self):
return UserProfile.objects.filter(addons=self,
@ -469,7 +473,7 @@ class Addon(amo.models.ModelBase):
@caching.cached_method
def share_counts(self):
rv = collections.defaultdict(int)
rv.update(ShareCountTotal.objects.filter(addon=self)
rv.update(AddonShareCountTotal.objects.filter(addon=self)
.values_list('service', 'count'))
return rv

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

@ -114,7 +114,7 @@
{% include 'addons/includes/collection_add_widget.html' %}
{% endif %}
{{ addon_sharing(addon) }}
{{ sharing_box(addon) }}
</div>{# /secondary #}

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

@ -73,7 +73,7 @@
{% endif %}
{# TODO(davedash): Remove until zamboni does sharing
{{ addon_sharing(addon) }}
{{ sharing_box(addon) }}
#}
</div></div>{# /addon-summary and -wrapper #}

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

@ -13,7 +13,7 @@ from cake.models import Session
from devhub.models import AddonLog, LOG as ADDONLOG
from files.models import TestResult, TestResultCache
from sharing.models import SERVICES
from stats.models import ShareCount, Contribution
from stats.models import AddonShareCount, Contribution
log = commonware.log.getLogger('z.cron')
@ -33,7 +33,7 @@ def gc(test_result=True):
Session.objects.filter(expires__lt=two_days_ago_unixtime).delete()
log.debug('Cleaning up sharing services.')
ShareCount.objects.exclude(
AddonShareCount.objects.exclude(
service__in=[s.shortname for s in SERVICES]).delete()
# XXX(davedash): I can't seem to run this during testing without triggering

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

@ -30,7 +30,7 @@
},
{
"pk": 2567,
"model": "stats.sharecount",
"model": "stats.addonsharecount",
"fields": {
"count": 1,
"date": "2009-07-27",

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

@ -5,7 +5,7 @@ from bandwagon.models import Collection
from cake.models import Session
from devhub.models import AddonLog
from files.models import TestResult, TestResultCache
from stats.models import ShareCount, Contribution
from stats.models import AddonShareCount, Contribution
from amo.cron import gc
@ -19,7 +19,7 @@ class GarbageTest(test_utils.TestCase):
eq_(AddonLog.objects.all().count(), 1)
eq_(TestResult.objects.all().count(), 1)
eq_(TestResultCache.objects.all().count(), 1)
eq_(ShareCount.objects.all().count(), 1)
eq_(AddonShareCount.objects.all().count(), 1)
eq_(Contribution.objects.all().count(), 1)
gc(test_result=False)
eq_(Collection.objects.all().count(), 0)
@ -28,5 +28,5 @@ class GarbageTest(test_utils.TestCase):
# XXX(davedash): this isn't working in testing.
# eq_(TestResult.objects.all().count(), 0)
eq_(TestResultCache.objects.all().count(), 0)
eq_(ShareCount.objects.all().count(), 0)
eq_(AddonShareCount.objects.all().count(), 0)
eq_(Contribution.objects.all().count(), 0)

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

@ -16,8 +16,9 @@ from amo.utils import sorted_groupby
from amo.urlresolvers import reverse
from addons.models import Addon, AddonRecommendation
from applications.models import Application
from users.models import UserProfile
from stats.models import CollectionShareCountTotal
from translations.fields import TranslatedField, LinkifiedField
from users.models import UserProfile
SPECIAL_SLUGS = amo.COLLECTION_SPECIAL_SLUGS
@ -177,6 +178,10 @@ class Collection(amo.models.ModelBase):
return reverse('collections.delete',
args=[self.author_username, self.slug])
def share_url(self):
return reverse('collections.share',
args=[self.author_username, self.slug])
@property
def author_username(self):
return self.author.username if self.author else 'anonymous'
@ -203,6 +208,13 @@ class Collection(amo.models.ModelBase):
else:
return settings.MEDIA_URL + 'img/amo2009/icons/collection.png'
@caching.cached_method
def share_counts(self):
rv = collections.defaultdict(int)
rv.update(CollectionShareCountTotal.objects.filter(collection=self)
.values_list('service', 'count'))
return rv
def get_recommendations(self):
"""Get a collection of recommended add-ons for this collection."""
if self.recommended_collection:

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

@ -33,6 +33,7 @@
<a title="{{ _('Share this Collection') }}" class="share" href="#"></a>
<a title="{{ _('Copy this Collection') }}" class="copy" href="#"></a>
#}
{{ sharing_box(c, show_email=False) }}
{% if request.check_ownership(c, require_owner=False) %}
<a title="{{ _('Edit this Collection') }}" class="edit" href="{{ c.edit_url() }}"></a>
{% endif %}

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

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import json
import urlparse
import django.test
from django.utils.datastructures import MultiValueDict
@ -670,3 +671,25 @@ class TestWatching(test_utils.TestCase):
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
eq_(r.status_code, 200)
eq_(json.loads(r.content), {'watching': True})
class TestSharing(test_utils.TestCase):
fixtures = ['base/collection_57181']
def test_twitter_share(self):
c = Collection.objects.get(id=57181)
r = self.client.get(c.share_url() + '?service=twitter')
eq_(r.status_code, 302)
loc = urlparse.urlparse(r['Location'])
query = dict(urlparse.parse_qsl(loc.query))
eq_(loc.netloc, 'twitter.com')
status = 'Home Business Auto :: Add-ons for Firefox'
assert status in query['status'], query['status']
def test_404(self):
c = Collection.objects.get(id=57181)
url = reverse('collections.share', args=[c.author.username, c.slug])
r = self.client.get(url)
eq_(r.status_code, 404)
r = self.client.get(url + '?service=xxx')
eq_(r.status_code, 404)

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

@ -19,6 +19,7 @@ detail_urls = patterns('',
url('^(?P<action>add|remove)$', views.collection_alter,
name='collections.alter'),
url('^watch$', views.watch, name='collections.watch'),
url('^share$', views.share, name='collections.share'),
)
ajax_urls = patterns('',

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

@ -11,6 +11,7 @@ import caching.base as caching
from tower import ugettext_lazy as _lazy, ugettext as _
import amo.utils
import sharing.views
from amo.decorators import login_required, post_required, json_view
from amo.urlresolvers import reverse
from access import acl
@ -455,3 +456,10 @@ def watch(request, username, slug):
return {'watching': watching}
else:
return redirect(collection.get_url_path())
def share(request, username, slug):
collection = get_collection(request, username, slug)
return sharing.views.share(request, collection,
name=collection.name,
description=collection.description)

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

@ -1,7 +1,7 @@
[
{
"pk": 177155738,
"model": "stats.sharecounttotal",
"model": "stats.addonsharecounttotal",
"fields": {
"count": 13,
"service": "delicious",
@ -10,7 +10,7 @@
},
{
"pk": 177155739,
"model": "stats.sharecounttotal",
"model": "stats.addonsharecounttotal",
"fields": {
"count": 29,
"service": "digg",
@ -19,7 +19,7 @@
},
{
"pk": 177155741,
"model": "stats.sharecounttotal",
"model": "stats.addonsharecounttotal",
"fields": {
"count": 4,
"service": "friendfeed",
@ -28,7 +28,7 @@
},
{
"pk": 177155742,
"model": "stats.sharecounttotal",
"model": "stats.addonsharecounttotal",
"fields": {
"count": 14,
"service": "myspace",
@ -37,7 +37,7 @@
},
{
"pk": 177155743,
"model": "stats.sharecounttotal",
"model": "stats.addonsharecounttotal",
"fields": {
"count": 4,
"service": "twitter",

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

@ -6,29 +6,33 @@ from amo.helpers import login_link
from .models import ServiceBase, EMAIL
@register.inclusion_tag('sharing/addon_sharing.html')
@register.inclusion_tag('sharing/sharing_box.html')
@jinja2.contextfunction
def addon_sharing(context, addon):
# prepare services
def sharing_box(context, obj, show_email=True):
request = context['request']
opts = {}
services = list(sharing.SERVICES_LIST)
if not show_email:
services.remove(EMAIL)
for service in sharing.SERVICES_LIST:
service_opts = {}
if service == EMAIL and not context['request'].user.is_authenticated():
if service == EMAIL and not request.user.is_authenticated():
service_opts['url'] = login_link(context)
service_opts['target'] = '_self'
else:
service_opts['url'] = '/addon/share/{id}?service={name}'.format(
id=addon.id, name=service.shortname)
url = obj.share_url() + '?service=%s' % service.shortname
service_opts['url'] = url
service_opts['target'] = '_blank'
opts[service] = service_opts
c = dict(context.items())
c.update({
'request': context['request'],
'user': context['request'].user,
'addon': addon,
'services': sharing.SERVICES_LIST,
'request': request,
'user': request.user,
'obj': obj,
'services': services,
'service_opts': opts,
'email_service': EMAIL,
'show_email': show_email,
})
return c

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

@ -1,5 +1,5 @@
from tower import ugettext_lazy as _, ungettext as ngettext
from stats.models import ShareCountTotal
from stats.models import AddonShareCountTotal
# string replacements in URLs are: url, title, description
@ -16,16 +16,16 @@ class DELICIOUS(ServiceBase):
"""see: http://delicious.com/help/savebuttons"""
shortname = 'delicious'
label = _(u'Add to Delicious')
url = ('http://delicious.com/save?url={url}&title={title}'
'&notes={description}')
url = (u'http://delicious.com/save?url={url}&title={title}'
'&notes={description}')
class DIGG(ServiceBase):
"""see: http://digg.com/tools/integrate#3"""
shortname = 'digg'
label = _(u'Digg this!')
url = ('http://digg.com/submit?url={url}&title={title}&bodytext='
'{description}&media=news&topic=tech_news')
url = (u'http://digg.com/submit?url={url}&title={title}&bodytext='
'{description}&media=news&topic=tech_news')
@staticmethod
def count_term(count):
@ -36,14 +36,14 @@ class FACEBOOK(ServiceBase):
"""see: http://www.facebook.com/share_options.php"""
shortname = 'facebook'
label = _(u'Post to Facebook')
url = 'http://www.facebook.com/share.php?u={url}&t={title}'
url = u'http://www.facebook.com/share.php?u={url}&t={title}'
class FRIENDFEED(ServiceBase):
"""see: http://friendfeed.com/embed/link"""
shortname = 'friendfeed'
label = _(u'Share on FriendFeed')
url = 'http://friendfeed.com/?url={url}&title={title}'
url = u'http://friendfeed.com/?url={url}&title={title}'
@staticmethod
def count_term(count):
@ -54,14 +54,14 @@ class MYSPACE(ServiceBase):
"""see: http://www.myspace.com/posttomyspace"""
shortname = 'myspace'
label = _(u'Post to MySpace')
url = ('http://www.myspace.com/index.cfm?fuseaction=postto&t={title}'
'&c={description}&u={url}&l=1')
url = (u'http://www.myspace.com/index.cfm?fuseaction=postto&t={title}'
'&c={description}&u={url}&l=1')
class TWITTER(ServiceBase):
shortname = 'twitter'
label = _(u'Post to Twitter')
url = 'https://twitter.com/home?status={title}%20{title}'
url = u'https://twitter.com/home?status={title}%20{url}'
@staticmethod
def count_term(count):

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

@ -1,11 +1,16 @@
<div class="share-this">
<a class="share" href="#">{{ _('Share this Add-on') }}</a>
{% if obj.__class__.__name__ == 'Addon' %}
<a class="share" href="#">{{ _('Share this Add-on') }}</a>
{% elif obj.__class__.__name__ == 'Collection' %}
<a class="share" href="#">{{ _('Share this Collection') }}</a>
{% endif %}
<div class="share-arrow"><div class="share-frame">
{% cache addon %}
{% cache obj %}
<div class="share-networks share-content">
<ul>
{% for service in services %}
{% set opts = service_opts[service] %}
{% set share_counts = obj.share_counts() %}
<li class="{{ service.shortname }}">
<span class="share-link">
<a class="uniquify" target="{{ opts.target }}" href="{{ opts.url }}">
@ -13,7 +18,7 @@
</a>
</span>
<span class="share-count">
{{ service.count_term(addon.share_counts()[service.shortname]) }}
{{ service.count_term(share_counts[service.shortname]) }}
</span>
</li>
{% endfor %}
@ -21,7 +26,7 @@
</div>{# /share-networks #}
{% endcache %}
{% if request.user.is_authenticated() %}
{% if request.user.is_authenticated() and show_email %}
<div class="share-email share-content">
<form action="{{ remora_url(service_opts[email_service].url) }}"
method="post">

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

@ -2,7 +2,7 @@ from datetime import date, timedelta
from django import test
from django.contrib.auth.models import User as DjangoUser
from django.utils import translation
from django.utils import translation, encoding
import jingo
from mock import Mock
@ -12,15 +12,15 @@ from pyquery import PyQuery as pq
from addons.models import Addon
import amo
import sharing
from sharing.helpers import addon_sharing
from sharing.helpers import sharing_box
from sharing.models import DIGG, FACEBOOK
from stats.models import ShareCount
from stats.models import AddonShareCount
class SharingHelpersTestCase(test.TestCase):
fixtures = ['base/addon_3615']
def test_addon_sharing(self):
def test_sharing_box(self):
addon = Addon.objects.get(id=3615)
jingo.load_helpers()
@ -37,7 +37,7 @@ class SharingHelpersTestCase(test.TestCase):
cake_csrf_token.__name__ = 'cake_csrf_token'
jingo.register.function(cake_csrf_token)
doc = pq(addon_sharing(ctx, addon))
doc = pq(sharing_box(ctx, addon))
self.assert_(doc.html())
self.assertEquals(doc('li').length, len(sharing.SERVICES_LIST))
@ -60,3 +60,27 @@ class SharingModelsTestCase(test.TestCase):
# total count with no shares
eq_(addon.share_counts()[FACEBOOK.shortname], 0,
'Total count with no shares must be 0')
def test_services_unicode():
u = u'\u05d0\u05d5\u05e1\u05e3'
d = dict(title=u, url=u, description=u)
for service in sharing.SERVICES_LIST:
if service.url:
service.url.format(**d)
# This does not work since Python tries to use ascii to decode the string.
# d = dict((k, encoding.smart_str(v)) for k, v in d.items())
# for service in sharing.SERVICES_LIST:
# if service.url:
# service.url.format(**d)
def test_share_view():
u = u'\u05d0\u05d5\u05e1\u05e3'
s = encoding.smart_str(u)
request, obj = Mock(), Mock()
request.GET = {'service': 'twitter'}
obj.get_url_path.return_value = u
sharing.views.share(request, obj, u, u)
obj.get_url_path.return_value = s
sharing.views.share(request, obj, s, s)

19
apps/sharing/views.py Normal file
Просмотреть файл

@ -0,0 +1,19 @@
from django import http
from django.shortcuts import redirect
from django.utils.encoding import smart_unicode as u
from amo.helpers import page_title, absolutify
import sharing
def share(request, obj, name, description):
try:
service = sharing.SERVICES[request.GET['service']]
except KeyError:
raise http.Http404()
d = {
'title': page_title({'request': request}, name),
'description': u(description),
'url': absolutify(u(obj.get_url_path())),
}
return redirect(service.url.format(**d))

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

@ -88,7 +88,7 @@ class UpdateCount(caching.base.CachingMixin, models.Model):
return ['*/addon/%d/statistics/usage*' % self.addon_id, ]
class ShareCount(caching.base.CachingMixin, models.Model):
class AddonShareCount(caching.base.CachingMixin, models.Model):
addon = models.ForeignKey('addons.Addon')
count = models.PositiveIntegerField()
service = models.CharField(max_length=255, null=True)
@ -101,7 +101,7 @@ class ShareCount(caching.base.CachingMixin, models.Model):
db_table = 'stats_share_counts'
class ShareCountTotal(caching.base.CachingMixin, models.Model):
class AddonShareCountTotal(caching.base.CachingMixin, models.Model):
addon = models.ForeignKey('addons.Addon')
count = models.PositiveIntegerField()
service = models.CharField(max_length=255, null=True)
@ -113,6 +113,18 @@ class ShareCountTotal(caching.base.CachingMixin, models.Model):
db_table = 'stats_share_counts_totals'
# stats_collections_share_counts exists too, but we don't touch it.
class CollectionShareCountTotal(caching.base.CachingMixin, models.Model):
collection = models.ForeignKey('bandwagon.Collection')
count = models.PositiveIntegerField()
service = models.CharField(max_length=255, null=True)
objects = caching.base.CachingManager()
class Meta:
db_table = 'stats_collections_share_counts_totals'
class ContributionError(Exception):
def __init__(self, value):

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

@ -0,0 +1,23 @@
DROP TABLE IF EXISTS `stats_collections_share_counts`;
CREATE TABLE `stats_collections_share_counts` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`collection_id` int(11) unsigned NOT NULL DEFAULT '0',
`count` int(11) unsigned NOT NULL DEFAULT '0',
`service` varchar(128) DEFAULT NULL,
`date` date NOT NULL DEFAULT '0000-00-00',
PRIMARY KEY (`id`),
UNIQUE KEY (`collection_id`, `service`, `date`),
CONSTRAINT FOREIGN KEY (collection_id) REFERENCES collections (id),
KEY `date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `stats_collections_share_counts_totals`;
CREATE TABLE `stats_collections_share_counts_totals` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`collection_id` int(11) unsigned NOT NULL DEFAULT '0',
`count` int(11) unsigned NOT NULL DEFAULT '0',
`service` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`collection_id`, `service`),
CONSTRAINT FOREIGN KEY (collection_id) REFERENCES collections (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;