зеркало из https://github.com/mozilla/kitsune.git
Remove tweets button for Army of Awesome. Bug 624464.
This commit is contained in:
Родитель
391ac6fa39
Коммит
8dac105b96
|
@ -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
|
||||||
|
|
Загрузка…
Ссылка в новой задаче