Massage customercare models to make tweet replies more traversible.

Tweet now uses tweet_id as its pkey. The surrogate key wasn't really useful, and this makes it simple to turn reply_to into a Django ForeignKey so we can traverse it for the Answered and Unanswered filters. Cascading deletes are okay for replies; it doesn't make sense (UI or otherwise) to hang onto replies to tweets we no longer have.
This commit is contained in:
Erik Rose 2011-02-28 16:15:15 -08:00
Родитель 02b6b20d20
Коммит 602144100c
5 изменённых файлов: 36 добавлений и 46 удалений

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

@ -1,120 +1,109 @@
[
{
"pk": 30,
"pk": 25308717656,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25308717656,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Kudos to Brizzly - the application is much less buggy lately. No more double tweet boxes. No more extra UI garbage on my Firefox window.\", \"created_at\": \"Thu, 23 Sep 2010 13:52:40 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/876200349/tool-belt_Logo_normal.jpg\", \"source\": \"<a href="http://www.brizzly.com" rel="nofollow">Brizzly</a>\", \"from_user\": \"rossgk\", \"from_user_id\": 150857, \"to_user_id\": null, \"geo\": null, \"id\": 25308717656, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:52:40"
}
},
{
"pk": 23,
"pk": 25308845620,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25308845620,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Firefox\\u3000chrome\\u306b\\u4e57\\u308a\\u63db\\u3048\\u308b\\u304b\\u601d\\u6848\\u4e2d #firefox\", \"created_at\": \"Thu, 23 Sep 2010 13:54:12 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/1041604705/188292_8256_normal.jpg\", \"source\": \"<a href="http://www.echofon.com/" rel="nofollow">Echofon</a>\", \"from_user\": \"ikd_fine\", \"from_user_id\": 129866568, \"to_user_id\": null, \"geo\": null, \"id\": 25308845620, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:54:12"
}
},
{
"pk": 21,
"pk": 25308851981,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25308851981,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"We're noticing a lot of #Flash Errors in #Firefox in the last few days. Including the AS3 Scroll Bars on my site. Too many updates FTL.\", \"created_at\": \"Thu, 23 Sep 2010 13:54:17 +0000\", \"profile_image_url\": \"http://a3.twimg.com/profile_images/1037516415/IA_100x100_avtr_normal.png\", \"source\": \"<a href="http://twitter.com/">web</a>\", \"from_user\": \"runtime_iA\", \"from_user_id\": 130011759, \"to_user_id\": null, \"geo\": null, \"id\": 25308851981, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:54:17"
}
},
{
"pk": 19,
"pk": 25308865789,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25308865789,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Ok so it looks good in chrome, firefox and safari. \\nIt even seems to look ok in IE. (163 visitors to MDN using IE last month)\", \"created_at\": \"Thu, 23 Sep 2010 13:54:27 +0000\", \"profile_image_url\": \"http://a0.twimg.com/profile_images/533310872/twitterscotty_normal.jpg\", \"source\": \"<a href="http://kiwi-app.net" rel="nofollow">kiwi</a>\", \"from_user\": \"macdevnet\", \"from_user_id\": 28772, \"to_user_id\": null, \"geo\": null, \"id\": 25308865789, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:54:27"
}
},
{
"pk": 15,
"pk": 25308906635,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25308906635,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"I'm still partial to Firefox, but IE9 Beta is FAST!\", \"created_at\": \"Thu, 23 Sep 2010 13:54:56 +0000\", \"profile_image_url\": \"http://a2.twimg.com/profile_images/1120347326/cole_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"Kid_Zer0\", \"from_user_id\": 130442244, \"to_user_id\": null, \"geo\": null, \"id\": 25308906635, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:54:56"
}
},
{
"pk": 14,
"pk": 25308913992,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25308913992,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Hey guys...am thinking of switching from IE to Firefox. yay or nay?\", \"created_at\": \"Thu, 23 Sep 2010 13:55:02 +0000\", \"profile_image_url\": \"http://a2.twimg.com/profile_images/151180126/Lucid_normal.jpg\", \"source\": \"<a href="http://twitter.com/">web</a>\", \"from_user\": \"LucidLilith\", \"from_user_id\": 1884444, \"to_user_id\": null, \"geo\": null, \"id\": 25308913992, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:55:02"
}
},
{
"pk": 11,
"pk": 25309157145,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25309157145,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Just changed my firefox theme for the first time in months and months. I like my new one :)\", \"created_at\": \"Thu, 23 Sep 2010 13:57:58 +0000\", \"profile_image_url\": \"http://a3.twimg.com/profile_images/1093204039/Me_and_claw_normal.jpg\", \"source\": \"<a href="http://twitter.com/">web</a>\", \"from_user\": \"jamesgrant17\", \"from_user_id\": 4403196, \"to_user_id\": null, \"geo\": null, \"id\": 25309157145, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:57:58"
}
},
{
"pk": 101,
"pk": 25309168529,
"model": "customercare.tweet",
"fields": {
"locale": "ro",
"tweet_id": 25309168529,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Un tweet n romana #Firefox\", \"created_at\": \"Thu, 23 Sep 2010 13:58:06 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/1117809237/cool_cat_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"__jimcasey__\", \"from_user_id\": 142651388, \"to_user_id\": null, \"geo\": null, \"id\": 25309168521, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:58:06"
}
},
{
"pk": 102,
"pk": 25309168528,
"model": "customercare.tweet",
"fields": {
"locale": "ro",
"tweet_id": 25309168528,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Inca un tweet in Romana pentru Firefox!\", \"created_at\": \"Thu, 23 Sep 2010 13:54:56 +0000\", \"profile_image_url\": \"http://a2.twimg.com/profile_images/1120347326/cole_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"Kid_Zer0\", \"from_user_id\": 130442244, \"to_user_id\": null, \"geo\": null, \"id\": 25308906635, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-24 14:58:06"
}
},
{
"pk": 10,
"pk": 25309168521,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25309168521,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Looks like with #Firefox "Tabs on top" & "Hide Menubar" add-ons you can get same amount of browser space that you have in Chrome. Yay!\", \"created_at\": \"Thu, 23 Sep 2010 13:58:06 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/1117809237/cool_cat_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"__jimcasey__\", \"from_user_id\": 142651388, \"to_user_id\": null, \"geo\": null, \"id\": 25309168521, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 13:58:06"
}
},
{
"pk": 4,
"pk": 25309381333,
"model": "customercare.tweet",
"fields": {
"locale": "en",
"tweet_id": 25309381333,
"raw_json": "{\"iso_language_code\": \"en\", \"text\": \"On this day in 2002, first public version of Mozilla Firefox ('Phoenix 0.1') is released. Jose Canseco becomes first member of 40-40 club.\", \"created_at\": \"Thu, 23 Sep 2010 14:00:35 +0000\", \"profile_image_url\": \"http://a3.twimg.com/profile_images/407391555/643797910_fgRus-S_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"jasonboche\", \"from_user_id\": 1644275, \"to_user_id\": null, \"geo\": null, \"id\": 25309381333, \"metadata\": {\"result_type\": \"recent\"}}",
"reply_to": null,
"created": "2010-09-23 14:00:35"

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

@ -8,13 +8,12 @@ from sumo.models import ModelBase, LocaleField
class Tweet(ModelBase):
"""An entry on twitter."""
tweet_id = models.BigIntegerField(unique=True)
tweet_id = models.BigIntegerField(primary_key=True)
raw_json = models.TextField()
# This is different from our usual locale, so not using LocaleField.
locale = models.CharField(max_length=20, db_index=True)
created = models.DateTimeField(default=datetime.now, db_index=True)
reply_to = models.BigIntegerField(blank=True, null=True, default=None,
db_index=True)
reply_to = models.ForeignKey('self', null=True, related_name='replies')
hidden = models.BooleanField(default=False, db_index=True)
class Meta:

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

@ -69,9 +69,9 @@ class GetOldestTweetTestCase(TestCase):
fixtures = ['tweets.json']
def test_get_oldest_tweet_exists(self):
eq_(11, _get_oldest_tweet('en', 2).pk)
eq_(4, _get_oldest_tweet('en', 0).pk)
eq_(21, _get_oldest_tweet('en', 6).pk)
eq_(25309157145, _get_oldest_tweet('en', 2).pk)
eq_(25309381333, _get_oldest_tweet('en', 0).pk)
eq_(25308851981, _get_oldest_tweet('en', 6).pk)
def test_get_oldest_tweet_offset_too_big(self):
eq_(None, _get_oldest_tweet('en', 100))

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

@ -55,7 +55,7 @@ class TweetListTestCase(TestCase):
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.reply_to_id = 25309168529
tw.save()
r = self.client.post(

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

@ -37,18 +37,19 @@ def _tweet_for_template(tweet):
# Recursively fetch replies.
if settings.CC_SHOW_REPLIES:
replies = _get_tweets(limit=0, reply_to=tweet.tweet_id)
# If ever slow, optimize to do fewer queries.
replies = _get_tweets(limit=0, reply_to=tweet)
else:
replies = None
return {'profile_img': bleach.clean(data['profile_image_url']),
'user': bleach.clean(data['from_user']),
'text': bleach.clean(data['text']),
'id': int(tweet.tweet_id),
'id': tweet.pk,
'date': date,
'reply_count': len(replies) if replies else 0,
'replies': replies,
'reply_to': tweet.reply_to}
'reply_to': tweet.reply_to and tweet.reply_to.pk}
def _get_tweets(locale=settings.LANGUAGE_CODE,
@ -138,7 +139,7 @@ def twitter_post(request):
"""Post a tweet, and return a rendering of it (and any replies)."""
try:
reply_to = int(request.POST.get('reply_to', ''))
reply_to_id = int(request.POST.get('reply_to', ''))
except ValueError:
# L10n: the tweet needs to be a reply to another tweet.
return HttpResponseBadRequest(_('Reply-to is empty'))
@ -152,7 +153,7 @@ def twitter_post(request):
return HttpResponseBadRequest(_('Message is too long'))
try:
result = request.twitter.api.update_status(content, reply_to)
result = request.twitter.api.update_status(content, reply_to_id)
except tweepy.TweepError, e:
# L10n: {message} is an error coming from our twitter api library
return HttpResponseBadRequest(
@ -176,16 +177,16 @@ def twitter_post(request):
'from_user': author['screen_name'],
'profile_image_url': author['profile_image_url'],
}
# Tweet metadata
tweet_model_data = {
'tweet_id': status['id'],
'raw_json': json.dumps(raw_tweet_data),
'locale': author['lang'],
'created': status['created_at'],
'reply_to': reply_to,
}
tweet = Tweet(**tweet_model_data)
tweet.save()
# The tweet with id `reply_to_id` will not be missing from the DB unless
# the purge cron job has run since the user loaded the form and we are
# replying to a deleted tweet. TODO: Catch integrity error and log or
# something.
tweet = Tweet.objects.create(pk=status['id'],
raw_json=json.dumps(raw_tweet_data),
locale=author['lang'],
created=status['created_at'],
reply_to_id=reply_to_id)
# We could optimize by not encoding and then decoding JSON.
return jingo.render(request, 'customercare/tweets.html',
@ -210,11 +211,12 @@ def hide_tweet(request):
return HttpResponseBadRequest(_('Invalid ID.'))
try:
tweet = Tweet.objects.get(tweet_id=id)
tweet = Tweet.objects.get(pk=id)
except Tweet.DoesNotExist:
return HttpResponseNotFound(_('Invalid ID.'))
if tweet.reply_to or Tweet.objects.filter(reply_to=id).count():
if (tweet.reply_to is not None or
Tweet.objects.filter(reply_to=tweet).exists()):
return HttpResponseBadRequest(_('Tweets that are replies or have '
'replies must not be hidden.'))