Remove tweets button for Army of Awesome. Bug 624464.

This commit is contained in:
Fred Wenzel 2011-02-17 14:29:21 -08:00
Родитель 391ac6fa39
Коммит 8dac105b96
11 изменённых файлов: 176 добавлений и 12 удалений

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

@ -6,7 +6,7 @@ from .models import Tweet, CannedCategory, CannedResponse, CategoryMembership
class TweetAdmin(admin.ModelAdmin): class TweetAdmin(admin.ModelAdmin):
date_hierarchy = 'created' date_hierarchy = 'created'
list_display = ('tweet_id', '__unicode__', 'created', 'locale') list_display = ('tweet_id', '__unicode__', 'created', 'locale')
list_filter = ('locale',) list_filter = ('locale', 'hidden')
search_fields = ('raw_json',) search_fields = ('raw_json',)
admin.site.register(Tweet, TweetAdmin) admin.site.register(Tweet, TweetAdmin)

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

@ -15,6 +15,7 @@ class Tweet(ModelBase):
created = models.DateTimeField(default=datetime.now, db_index=True) created = models.DateTimeField(default=datetime.now, db_index=True)
reply_to = models.BigIntegerField(blank=True, null=True, default=None, reply_to = models.BigIntegerField(blank=True, null=True, default=None,
db_index=True) db_index=True)
hidden = models.BooleanField(default=False, db_index=True)
class Meta: class Meta:
get_latest_by = 'created' get_latest_by = 'created'

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

@ -45,6 +45,7 @@
<br style="clear:both; height: 1px" /> <br style="clear:both; height: 1px" />
<div id="tweets-wrap"> <div id="tweets-wrap">
{{ csrf() }}{# CSRF token for AJAX actions. #}
{% if not tweets %} {% if not tweets %}
<div class="warning-box"> <div class="warning-box">
{% trans language=settings.LOCALES[request.locale].native %} {% trans language=settings.LOCALES[request.locale].native %}

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

@ -21,6 +21,12 @@
{% elif settings.CC_SHOW_REPLIES %} {% elif settings.CC_SHOW_REPLIES %}
<span class="reply_count">{{ _('Reply now') }}</span> <span class="reply_count">{{ _('Reply now') }}</span>
{% endif %} {% endif %}
{% if settings.CC_ALLOW_REMOVE and not (tweet.reply_to or tweet.replies) %}
<a class="remove_tweet" href="{{ url('customercare.hide_tweet') }}">{{
_('Remove') }}</a>
{% endif %}
<p class="text">{{ tweet.text|safe }}</p> <p class="text">{{ tweet.text|safe }}</p>
</div> </div>
<div id="replies_{{ tweet.id }}" class="replies" data-tweet-id="{{ tweet.id }}"> <div id="replies_{{ tweet.id }}" class="replies" data-tweet-id="{{ tweet.id }}">

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

@ -1,7 +1,12 @@
from django.conf import settings
from mock import patch_object
from nose.tools import eq_ from nose.tools import eq_
from customercare.models import Tweet
from customercare.views import _get_tweets from customercare.views import _get_tweets
from sumo.tests import TestCase from sumo.tests import TestCase
from sumo.urlresolvers import reverse
class TweetListTestCase(TestCase): class TweetListTestCase(TestCase):
@ -28,3 +33,53 @@ class TweetListTestCase(TestCase):
# max_id. # max_id.
for tweet in tweets_2: for tweet in tweets_2:
assert tweet['id'] < max_id assert tweet['id'] < max_id
def test_hide_tweets(self):
"""Try hiding tweets."""
hide_tweet = lambda id: self.client.post(
reverse('customercare.hide_tweet', locale='en-US'),
{'id': id})
tw = Tweet.objects.no_cache().filter(reply_to=None, hidden=False)[0]
r = hide_tweet(tw.tweet_id)
eq_(r.status_code, 200)
# Re-fetch from database. Should be hidden.
tw = Tweet.objects.no_cache().get(tweet_id=tw.tweet_id)
eq_(tw.hidden, True)
# Hiding it again should work.
r = hide_tweet(tw.tweet_id)
eq_(r.status_code, 200)
def test_hide_tweets_with_replies(self):
"""Hiding tweets with replies is not allowed."""
tw = Tweet.objects.filter(reply_to=None)[0]
tw.reply_to = 123
tw.save()
r = self.client.post(
reverse('customercare.hide_tweet', locale='en-US'),
{'id': tw.tweet_id})
eq_(r.status_code, 400)
def test_hide_tweets_invalid_id(self):
"""Invalid tweet IDs shouldn't break anything."""
hide_tweet = lambda id: self.client.post(
reverse('customercare.hide_tweet', locale='en-US'),
{'id': id})
r = hide_tweet(123)
eq_(r.status_code, 404)
r = hide_tweet('cheesecake')
eq_(r.status_code, 400)
@patch_object(settings._wrapped, 'CC_ALLOW_REMOVE', False)
def test_hide_tweets_disabled(self):
"""Do not allow hiding tweets if feature is disabled."""
tw = Tweet.objects.filter(reply_to=None)[0]
r = self.client.post(
reverse('customercare.hide_tweet', locale='en-US'),
{'id': tw.tweet_id})
eq_(r.status_code, 418) # Don't tell a teapot to brew coffee.

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

@ -1,7 +1,8 @@
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('customercare.views', urlpatterns = patterns('customercare.views',
url(r'/more_tweets', 'more_tweets', name="customercare.more_tweets"), url(r'^/more_tweets$', 'more_tweets', name="customercare.more_tweets"),
url(r'/twitter_post', 'twitter_post', name="customercare.twitter_post"), url(r'^/twitter_post$', 'twitter_post', name="customercare.twitter_post"),
url(r'', 'landing', name='customercare.landing'), url(r'^/hide_tweet$', 'hide_tweet', name="customercare.hide_tweet"),
url(r'^$', 'landing', name='customercare.landing'),
) )

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

@ -6,7 +6,9 @@ import logging
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpResponseBadRequest from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseNotFound, HttpResponseServerError)
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST, require_GET from django.views.decorators.http import require_POST, require_GET
from babel.numbers import format_number from babel.numbers import format_number
@ -15,7 +17,7 @@ import jingo
from tower import ugettext as _ from tower import ugettext as _
import tweepy import tweepy
from .models import CannedCategory, Tweet from customercare.models import CannedCategory, Tweet
import twitter import twitter
@ -58,7 +60,7 @@ def _get_tweets(locale=settings.LANGUAGE_CODE,
max_id will only return tweets with the status ids less than the given id. max_id will only return tweets with the status ids less than the given id.
""" """
locale = settings.LOCALES[locale].iso639_1 locale = settings.LOCALES[locale].iso639_1
q = Tweet.objects.filter(locale=locale, reply_to=reply_to) q = Tweet.objects.filter(locale=locale, reply_to=reply_to, hidden=False)
if max_id: if max_id:
q = q.filter(tweet_id__lt=max_id) q = q.filter(tweet_id__lt=max_id)
if limit: if limit:
@ -188,3 +190,39 @@ def twitter_post(request):
# We could optimize by not encoding and then decoding JSON. # We could optimize by not encoding and then decoding JSON.
return jingo.render(request, 'customercare/tweets.html', return jingo.render(request, 'customercare/tweets.html',
{'tweets': [_tweet_for_template(tweet)]}) {'tweets': [_tweet_for_template(tweet)]})
@require_POST
def hide_tweet(request):
"""
Hide the tweet with a given ID. Only hides tweets that are not replies
and do not have replies.
Returns proper HTTP status codes.
"""
# If feature disabled, bail.
if not settings.CC_ALLOW_REMOVE:
return HttpResponse(status=418) # I'm a teapot.
try:
id = int(request.POST.get('id'))
except (ValueError, TypeError):
return HttpResponseBadRequest(_('Invalid ID.'))
try:
tweet = Tweet.objects.get(tweet_id=id)
except Tweet.DoesNotExist:
return HttpResponseNotFound(_('Invalid ID.'))
if tweet.reply_to or Tweet.objects.filter(reply_to=id).count():
return HttpResponseBadRequest(_('Tweets that are replies or have '
'replies must not be hidden.'))
try:
tweet.hidden = True
tweet.save(force_update=True)
except Exception, e:
return HttpResponseServerError(
_('An error occured: {message}').format(message=e))
return HttpResponse('ok')

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

@ -155,6 +155,10 @@ div.warning-box {
border-top: 1px solid #fef1ad; border-top: 1px solid #fef1ad;
} }
#tweets li {
position: relative;
}
#tweets li img { #tweets li img {
border: solid 1px #cacccb; border: solid 1px #cacccb;
float: left; float: left;
@ -193,7 +197,23 @@ div.warning-box {
width: 550px; width: 550px;
} }
#tweets li .reply_count { #tweets .remove_tweet {
display: none;
}
html.js #tweets li:hover .remove_tweet,
#tweets li .remove_tweet.clicked {
display: inline;
position: absolute;
right: 10px;
bottom: 10px;
}
#tweets li .remove_tweet.clicked {
padding-left: 20px;
background: url('../img/customercare/spinner.gif') left top no-repeat;
}
#tweets li .reply_count,
#tweets li .remove_tweet {
clear: right; clear: right;
float: right; float: right;
font-family: Verdana, sans-serif; font-family: Verdana, sans-serif;
@ -206,14 +226,20 @@ div.warning-box {
color: #0ba643; color: #0ba643;
} }
#tweets li a.reply_count:before { #tweets li a.reply_count:before {
content: '\25B6\A0'; content: '\25B6\A0'; /* rightarrow, space */
} }
#tweets li a.reply_count.opened:before { #tweets li a.reply_count.opened:before {
content: '\25BC\A0'; content: '\25BC\A0'; /* downarrow, space */
} }
#tweets li a.reply_count:hover { #tweets li .reply_count:hover,
#tweets li .remove_tweet:hover {
text-decoration: underline; text-decoration: underline;
} }
#tweets li .remove_tweet:before {
color: #e45d49;
content: '\2716\A0'; /* big X, space */
font-size: 110%;
}
.tweets-buttons { .tweets-buttons {
float: right; float: right;

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

@ -1,6 +1,7 @@
(function($){ (function($){
// Tweet IDs are too high. Using .data('tweet-id') returns incorrect // Tweet IDs are too high. Using .data('tweet-id') returns incorrect
// results. See jQuery bug 7579 - http://bugs.jquery.com/ticket/7579 // results. Use .attr('data-tweet-id') instead.
// See jQuery bug 7579 - http://bugs.jquery.com/ticket/7579
function Memory(name) { function Memory(name) {
this._id = null; this._id = null;
@ -373,6 +374,37 @@
e.preventDefault(); e.preventDefault();
}); });
/* Remove tweet functionality */
$('#tweets a.remove_tweet').live('click', function(e) {
if ($(this).hasClass('clicked')) return false;
$(this).addClass('clicked');
var tweet = $(this).closest('li'),
tweet_id = tweet.attr('data-tweet-id');
$.ajax({
url: $(this).attr('href'),
type: 'POST',
data: {
csrfmiddlewaretoken: $('#tweets-wrap input[name=csrfmiddlewaretoken]').val(),
id: tweet_id
},
dataType: 'text',
success: function() {
$(this).removeClass('clicked');
tweet.slideUp('fast', function() {
$(this).remove();
});
},
error: function(err) {
$(this).removeClass('clicked');
alert('Error removing tweet: ' + err.responseText);
}
});
$(this).blur();
e.preventDefault();
});
/* Search box */ /* Search box */
$('#side-search input[name="q"]').autoPlaceholderText(); $('#side-search input[name="q"]').autoPlaceholderText();

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

@ -0,0 +1,3 @@
-- Allow tweets to be hidden on Army of Awesome page.
ALTER TABLE `customercare_tweet` ADD `hidden` TINYINT(1) NOT NULL DEFAULT '0';
ALTER TABLE `customercare_tweet` ADD INDEX ( `hidden` ) ;

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

@ -568,6 +568,7 @@ VIDEO_MAX_FILESIZE = 16777216 # 16 megabytes, in bytes
CC_MAX_TWEETS = 500 # Max. no. of tweets in DB CC_MAX_TWEETS = 500 # Max. no. of tweets in DB
CC_TWEETS_PERPAGE = 100 # How many tweets to collect in one go. Max: 100. CC_TWEETS_PERPAGE = 100 # How many tweets to collect in one go. Max: 100.
CC_SHOW_REPLIES = True # Show replies to tweets? CC_SHOW_REPLIES = True # Show replies to tweets?
CC_ALLOW_REMOVE = True # Allow users to hide tweets?
CC_TWEET_ACTIVITY_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeKillRate.json' # Tweet activity stats CC_TWEET_ACTIVITY_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeKillRate.json' # Tweet activity stats
CC_TOP_CONTRIB_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeTopSoldiers.json' # Top contributor stats CC_TOP_CONTRIB_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeTopSoldiers.json' # Top contributor stats