# HG changeset patch # User Brian Neal # Date 1431566963 18000 # Node ID 7ab180ff6f7b5c704799b5a1d15a74d87768f952 # Parent cf9918328c640e07b7b336637a5e1857f0f672a0# Parent de4425ad62fbac0cc35f6f9b0a92c70e3cb1416e Merge upstream. diff -r de4425ad62fb -r 7ab180ff6f7b antispam/__init__.py --- a/antispam/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/antispam/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,12 +1,6 @@ -from django.contrib.auth import views as auth_views - -from antispam.decorators import log_auth_failures -import antispam.receivers +default_app_config = 'antispam.apps.AntiSpamConfig' SPAM_PHRASE_KEY = "antispam.spam_phrases" BUSTED_MESSAGE = ("Your post has tripped our spam filter. Your account has " "been suspended pending a review of your post. If this was a mistake " "then we apologize; your account will be restored shortly.") - -# Install rate limiting on auth login -auth_views.login = log_auth_failures('Login')(auth_views.login) diff -r de4425ad62fb -r 7ab180ff6f7b antispam/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/antispam/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,16 @@ +from django.apps import AppConfig +import django.contrib.auth.views + +from antispam.decorators import log_auth_failures + + +class AntiSpamConfig(AppConfig): + name = 'antispam' + verbose_name = 'Anti-Spam' + + def ready(self): + import antispam.receivers + + # Install rate limiting on auth login + django.contrib.auth.views.login = log_auth_failures('Login')( + django.contrib.auth.views.login) diff -r de4425ad62fb -r 7ab180ff6f7b antispam/receivers.py --- a/antispam/receivers.py Wed May 13 20:27:17 2015 -0500 +++ b/antispam/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -24,6 +24,7 @@ if user: logger.info('User logout signal: %s', user.username) + def login_failed_callback(sender, credentials, **kwargs): """Signal callback for a login failure event.""" logger.error('User login failed signal from %s: %s', sender, diff -r de4425ad62fb -r 7ab180ff6f7b antispam/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/antispam/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,33 @@ +"""Tests for the antispam signal handlers.""" +import logging + +from django.contrib.auth.models import User +from django.test import TestCase + +from testfixtures import log_capture + + +class AntispamSignalRcvrTestCase(TestCase): + + def setUp(self): + self.user = User.objects.create_user('steve', 'steve@example.com', 'pwd') + + # Temporarily enable logging + self.old_disable = logging.getLogger().manager.disable + logging.disable(logging.NOTSET) + + def tearDown(self): + logging.disable(self.old_disable) + + @log_capture('auth') + def test_login_logout_callback(self, lc): + self.assertTrue(self.client.login(username='steve', password='pwd')) + self.client.logout() + lc.check(('auth', 'INFO', 'User login signal: steve'), + ('auth', 'INFO', 'User logout signal: steve')) + + @log_capture('auth') + def test_login_failed_callback(self, lc): + self.assertFalse(self.client.login(username='steve', password='xxx')) + lc.check(('auth', 'ERROR', + 'User login failed signal from django.contrib.auth: steve')) diff -r de4425ad62fb -r 7ab180ff6f7b bio/__init__.py --- a/bio/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/bio/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,1 +1,1 @@ -import signals +default_app_config = 'bio.apps.BioConfig' diff -r de4425ad62fb -r 7ab180ff6f7b bio/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bio/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class BioConfig(AppConfig): + name = 'bio' + verbose_name = 'Biography' + + def ready(self): + import bio.receivers diff -r de4425ad62fb -r 7ab180ff6f7b bio/models.py --- a/bio/models.py Wed May 13 20:27:17 2015 -0500 +++ b/bio/models.py Wed May 13 20:29:23 2015 -0500 @@ -13,6 +13,7 @@ from core.markup import SiteMarkup import bio.flags +from bio.signals import notify_profile_content_update # These are the secondary user status enumeration values. @@ -221,6 +222,3 @@ if self.count == 1: return u"1 %s" % self.badge.name return u"%d %ss" % (self.count, self.badge.name) - -# Put down here to avoid a circular import -from bio.signals import notify_profile_content_update diff -r de4425ad62fb -r 7ab180ff6f7b bio/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bio/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,95 @@ +""" +Signal handlers & signals for the bio application. + +""" +from django.db.models.signals import post_save +from django.contrib.auth.models import User + +import bio.badges +from bio.models import UserProfile + +from donations.models import Donation +from weblinks.models import Link +from downloads.models import Download +from news.models import Story +from potd.models import Photo + + +def on_user_save(sender, **kwargs): + """ + This signal handler ensures that every User has a corresonding + UserProfile. It is called after User instance is saved. It creates + a UserProfile for the User if the created argument is True. + + """ + created = kwargs['created'] + if created: + user = kwargs['instance'] + profile = UserProfile() + profile.user = user + profile.save() + + +def on_donation_save(sender, **kwargs): + """ + This function is called after a Donation is saved. + If the Donation was newly created and not anonymous, + award the user a contributor pin. + + """ + if kwargs['created']: + donation = kwargs['instance'] + if not donation.is_anonymous and donation.user: + bio.badges.award_badge(bio.badges.CONTRIBUTOR_PIN, donation.user) + + +def on_link_save(sender, **kwargs): + """ + This function is called after a Link is saved. If the Link was newly + created, award the user a link pin. + + """ + if kwargs['created']: + link = kwargs['instance'] + bio.badges.award_badge(bio.badges.LINK_PIN, link.user) + + +def on_download_save(sender, **kwargs): + """ + This function is called after a Download is saved. If the Download was + newly created, award the user a download pin. + + """ + if kwargs['created']: + download = kwargs['instance'] + bio.badges.award_badge(bio.badges.DOWNLOAD_PIN, download.user) + + +def on_story_save(sender, **kwargs): + """ + This function is called after a Story is saved. If the Story was + newly created, award the user a news pin. + + """ + if kwargs['created']: + story = kwargs['instance'] + bio.badges.award_badge(bio.badges.NEWS_PIN, story.submitter) + + +def on_photo_save(sender, **kwargs): + """ + This function is called after a Photo is saved. If the Photo was + newly created, award the user a POTD pin. + + """ + if kwargs['created']: + photo = kwargs['instance'] + bio.badges.award_badge(bio.badges.POTD_PIN, photo.user) + + +post_save.connect(on_user_save, sender=User, dispatch_uid='bio.receivers') +post_save.connect(on_donation_save, sender=Donation, dispatch_uid='bio.receivers') +post_save.connect(on_link_save, sender=Link, dispatch_uid='bio.receivers') +post_save.connect(on_download_save, sender=Download, dispatch_uid='bio.receivers') +post_save.connect(on_story_save, sender=Story, dispatch_uid='bio.receivers') +post_save.connect(on_photo_save, sender=Photo, dispatch_uid='bio.receivers') diff -r de4425ad62fb -r 7ab180ff6f7b bio/search_indexes.py --- a/bio/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/bio/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -2,10 +2,11 @@ from haystack import indexes from bio.models import UserProfile +from custom_search.fields import MaxTermSizeCharField class UserProfileIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) author = indexes.CharField(model_attr='user') def get_model(self): diff -r de4425ad62fb -r 7ab180ff6f7b bio/signals.py --- a/bio/signals.py Wed May 13 20:27:17 2015 -0500 +++ b/bio/signals.py Wed May 13 20:29:23 2015 -0500 @@ -1,98 +1,8 @@ """ -Signal handlers & signals for the bio application. - +Signals for the bio application """ -from django.db.models.signals import post_save -from django.contrib.auth.models import User import django.dispatch -from donations.models import Donation -from weblinks.models import Link -from downloads.models import Download -from news.models import Story -from potd.models import Photo - - -def on_user_save(sender, **kwargs): - """ - This signal handler ensures that every User has a corresonding - UserProfile. It is called after User instance is saved. It creates - a UserProfile for the User if the created argument is True. - - """ - created = kwargs['created'] - if created: - user = kwargs['instance'] - profile = UserProfile() - profile.user = user - profile.save() - - -def on_donation_save(sender, **kwargs): - """ - This function is called after a Donation is saved. - If the Donation was newly created and not anonymous, - award the user a contributor pin. - - """ - if kwargs['created']: - donation = kwargs['instance'] - if not donation.is_anonymous and donation.user: - bio.badges.award_badge(bio.badges.CONTRIBUTOR_PIN, donation.user) - - -def on_link_save(sender, **kwargs): - """ - This function is called after a Link is saved. If the Link was newly - created, award the user a link pin. - - """ - if kwargs['created']: - link = kwargs['instance'] - bio.badges.award_badge(bio.badges.LINK_PIN, link.user) - - -def on_download_save(sender, **kwargs): - """ - This function is called after a Download is saved. If the Download was - newly created, award the user a download pin. - - """ - if kwargs['created']: - download = kwargs['instance'] - bio.badges.award_badge(bio.badges.DOWNLOAD_PIN, download.user) - - -def on_story_save(sender, **kwargs): - """ - This function is called after a Story is saved. If the Story was - newly created, award the user a news pin. - - """ - if kwargs['created']: - story = kwargs['instance'] - bio.badges.award_badge(bio.badges.NEWS_PIN, story.submitter) - - -def on_photo_save(sender, **kwargs): - """ - This function is called after a Photo is saved. If the Photo was - newly created, award the user a POTD pin. - - """ - if kwargs['created']: - photo = kwargs['instance'] - bio.badges.award_badge(bio.badges.POTD_PIN, photo.user) - - -post_save.connect(on_user_save, sender=User, dispatch_uid='bio.signals') -post_save.connect(on_donation_save, sender=Donation, dispatch_uid='bio.signals') -post_save.connect(on_link_save, sender=Link, dispatch_uid='bio.signals') -post_save.connect(on_download_save, sender=Download, dispatch_uid='bio.signals') -post_save.connect(on_story_save, sender=Story, dispatch_uid='bio.signals') -post_save.connect(on_photo_save, sender=Photo, dispatch_uid='bio.signals') - -# Signals for the bio application # # This signal is sent whenever a profile has had its textual content updated. # The provided arguments to the receiver function are: @@ -107,8 +17,3 @@ """ profile_content_update.send_robust(profile) - - -# To avoid circular imports -import bio.badges -from bio.models import UserProfile diff -r de4425ad62fb -r 7ab180ff6f7b bio/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bio/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,168 @@ +"""Tests for the bio app's signal handlers.""" + +from collections import namedtuple + +from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.test import TestCase + +import bio.badges +from bio.models import Badge +from bio.models import BadgeOwnership +from bio.models import UserProfile +import custom_search.receivers +from donations.models import Donation +from downloads.models import Download +import downloads.receivers +from news.models import Story +from potd.models import Photo +import potd.receivers +from weblinks.models import Link +import weblinks.receivers + + +FakeDonation = namedtuple('FakeDonation', ['is_anonymous', 'user']) +FakeUserObject = namedtuple('FakeUserObject', ['user']) +FakeStory = namedtuple('FakeStory', ['submitter']) + + +class ReceiverTestCase(TestCase): + + fixtures = ['badges.json'] + + def setUp(self): + self.user = User.objects.create_user('user', 'user@example.com', 'pw') + + # Don't let our custom search signal handler class catch any of the + # signals we are throwing here. + custom_search.receivers.signal_processor.teardown() + + # Don't let these signal handlers fire either + post_save.disconnect(sender=Link, dispatch_uid='weblinks.receivers') + post_save.disconnect(sender=Download, dispatch_uid='downloads.receivers') + post_save.disconnect(sender=Photo, dispatch_uid='potd.receivers') + + def tearDown(self): + custom_search.receivers.signal_processor.setup() + post_save.connect(weblinks.receivers.on_link_save, sender=Link, + dispatch_uid='weblinks.receivers') + post_save.connect(downloads.receivers.on_download_save, sender=Download, + dispatch_uid='downloads.receivers') + post_save.connect(potd.receivers.on_photo_save, sender=Photo, + dispatch_uid='potd.receivers') + + def test_profile_creation(self): + profile = UserProfile.objects.get(user=self.user) + self.assertEqual(self.user.profile, profile) + + def test_donation_created(self): + donation = FakeDonation(False, self.user) + post_save.send(sender=Donation, created=True, instance=donation) + + badge = Badge.objects.get(numeric_id=bio.badges.CONTRIBUTOR_PIN) + ownership = BadgeOwnership.objects.get(badge=badge, profile=self.user.profile) + self.assertEqual(ownership.count, 1) + + def test_donation_updated(self): + donation = FakeDonation(False, self.user) + post_save.send(sender=Donation, created=False, instance=donation) + + badge = Badge.objects.get(numeric_id=bio.badges.CONTRIBUTOR_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_donation_anonymous(self): + donation = FakeDonation(True, self.user) + post_save.send(sender=Donation, created=False, instance=donation) + + badge = Badge.objects.get(numeric_id=bio.badges.CONTRIBUTOR_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_donation_no_user(self): + donation = FakeDonation(False, None) + post_save.send(sender=Donation, created=False, instance=donation) + + badge = Badge.objects.get(numeric_id=bio.badges.CONTRIBUTOR_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_donation_anon_and_no_user(self): + donation = FakeDonation(True, None) + post_save.send(sender=Donation, created=False, instance=donation) + + badge = Badge.objects.get(numeric_id=bio.badges.CONTRIBUTOR_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_link_created(self): + link = FakeUserObject(self.user) + post_save.send(sender=Link, created=True, instance=link) + + badge = Badge.objects.get(numeric_id=bio.badges.LINK_PIN) + ownership = BadgeOwnership.objects.get(badge=badge, profile=self.user.profile) + self.assertEqual(ownership.count, 1) + + def test_link_updated(self): + link = FakeUserObject(self.user) + post_save.send(sender=Link, created=False, instance=link) + + badge = Badge.objects.get(numeric_id=bio.badges.LINK_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_download_created(self): + dl = FakeUserObject(self.user) + post_save.send(sender=Download, created=True, instance=dl) + + badge = Badge.objects.get(numeric_id=bio.badges.DOWNLOAD_PIN) + ownership = BadgeOwnership.objects.get(badge=badge, profile=self.user.profile) + self.assertEqual(ownership.count, 1) + + def test_download_updated(self): + dl = FakeUserObject(self.user) + post_save.send(sender=Download, created=False, instance=dl) + + badge = Badge.objects.get(numeric_id=bio.badges.DOWNLOAD_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_story_created(self): + story = FakeStory(self.user) + post_save.send(sender=Story, created=True, instance=story) + + badge = Badge.objects.get(numeric_id=bio.badges.NEWS_PIN) + ownership = BadgeOwnership.objects.get(badge=badge, profile=self.user.profile) + self.assertEqual(ownership.count, 1) + + def test_story_updated(self): + story = FakeStory(self.user) + post_save.send(sender=Story, created=False, instance=story) + + badge = Badge.objects.get(numeric_id=bio.badges.NEWS_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) + + def test_photo_created(self): + photo = FakeUserObject(self.user) + post_save.send(sender=Photo, created=True, instance=photo) + + badge = Badge.objects.get(numeric_id=bio.badges.POTD_PIN) + ownership = BadgeOwnership.objects.get(badge=badge, profile=self.user.profile) + self.assertEqual(ownership.count, 1) + + def test_photo_updated(self): + photo = FakeUserObject(self.user) + post_save.send(sender=Photo, created=False, instance=photo) + + badge = Badge.objects.get(numeric_id=bio.badges.POTD_PIN) + self.assertRaises(BadgeOwnership.DoesNotExist, + BadgeOwnership.objects.get, + badge=badge, profile=self.user.profile) diff -r de4425ad62fb -r 7ab180ff6f7b contests/models.py --- a/contests/models.py Wed May 13 20:27:17 2015 -0500 +++ b/contests/models.py Wed May 13 20:29:23 2015 -0500 @@ -30,7 +30,7 @@ title = models.CharField(max_length=64) slug = models.SlugField(max_length=64) description = models.TextField() - is_public = models.BooleanField(db_index=True) + is_public = models.BooleanField(default=False, db_index=True) creation_date = models.DateTimeField(blank=True) end_date = models.DateTimeField() contestants = models.ManyToManyField(User, related_name='contests', diff -r de4425ad62fb -r 7ab180ff6f7b core/markup.py --- a/core/markup.py Wed May 13 20:27:17 2015 -0500 +++ b/core/markup.py Wed May 13 20:29:23 2015 -0500 @@ -5,7 +5,7 @@ import markdown as _markdown from django.utils.encoding import force_unicode -from smiley import SmilifyMarkdown +from smiley.utils import SmilifyMarkdown from core.mdexts.urlize import UrlizeExtension from core.mdexts.deleted import DelExtension from core.html import clean_html diff -r de4425ad62fb -r 7ab180ff6f7b core/tests/test_ssl_images.py --- a/core/tests/test_ssl_images.py Wed May 13 20:27:17 2015 -0500 +++ b/core/tests/test_ssl_images.py Wed May 13 20:29:23 2015 -0500 @@ -123,8 +123,6 @@ self.assertEqual(expected, result) @mock.patch('core.management.commands.ssl_images.save_image_to_cloud') - @mock.patch('core.management.commands.ssl_images.check_https_availability', - new=lambda r: None) def test_simple_replacement(self, upload_mock): old_src = 'http://example.com/images/my_image.jpg' new_src = 'https://cloud.com/ABCDEF.jpg' @@ -138,11 +136,9 @@ upload_mock.return_value = new_src result = process_post(test_str) self.assertEqual(expected, result) - upload_mock.assert_called_once_with(old_src) + upload_mock.assert_called_once_with(urlparse(old_src)) @mock.patch('core.management.commands.ssl_images.save_image_to_cloud') - @mock.patch('core.management.commands.ssl_images.check_https_availability', - new=lambda r: None) def test_multiple_replacement(self, upload_mock): old_src = [ 'http://example.com/images/my_image.jpg', @@ -169,12 +165,10 @@ upload_mock.side_effect = new_src result = process_post(test_str) self.assertEqual(expected, result) - expected_args = [mock.call(c) for c in old_src] + expected_args = [mock.call(urlparse(c)) for c in old_src] self.assertEqual(upload_mock.call_args_list, expected_args) @mock.patch('core.management.commands.ssl_images.save_image_to_cloud') - @mock.patch('core.management.commands.ssl_images.check_https_availability', - new=lambda r: None) def test_multiple_replacement_2(self, upload_mock): old_src = [ 'http://example.com/images/my_image.jpg', @@ -203,11 +197,9 @@ upload_mock.side_effect = new_src result = process_post(test_str) self.assertEqual(expected, result) - upload_mock.assert_called_once_with(old_src[0]) + upload_mock.assert_called_once_with(urlparse(old_src[0])) @mock.patch('core.management.commands.ssl_images.save_image_to_cloud') - @mock.patch('core.management.commands.ssl_images.check_https_availability', - new=lambda r: None) def test_caching(self, upload_mock): old_src = [ 'http://example.com/images/my_image.jpg', @@ -234,39 +226,9 @@ upload_mock.side_effect = new_src result = process_post(test_str) self.assertEqual(expected, result) - expected_args = [mock.call(c) for c in old_src[:2]] + expected_args = [mock.call(urlparse(c)) for c in old_src[:2]] self.assertEqual(upload_mock.call_args_list, expected_args) - @mock.patch('core.management.commands.ssl_images.check_https_availability') - def test_https_availability(self, check_https_mock): - old_src = [ - 'http://example.com/images/my_image.jpg', - 'http://example.com/static/wow.gif', - 'http://example.com/images/another_image.jpg', - ] - new_src = [ - 'https://example.com/images/my_image.jpg', - 'https://example.com/static/wow.gif', - 'https://example.com/images/another_image.jpg', - ] - - template = """Here is a really cool http: based image: - ![flyer]({}) - Cool, right? - Another one: ![pic]({}) - And finally - ![an image]({}) - """ - - test_str = template.format(*old_src) - expected = template.format(*new_src) - - check_https_mock.side_effect = new_src - result = process_post(test_str) - self.assertEqual(expected, result) - expected_args = [mock.call(urlparse(c)) for c in old_src] - self.assertEqual(check_https_mock.call_args_list, expected_args) - class HtmlCheckTestCase(unittest.TestCase): diff -r de4425ad62fb -r 7ab180ff6f7b custom_search/__init__.py --- a/custom_search/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/custom_search/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,1 @@ +default_app_config = 'custom_search.apps.CustomSearchConfig' diff -r de4425ad62fb -r 7ab180ff6f7b custom_search/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/custom_search/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class CustomSearchConfig(AppConfig): + name = 'custom_search' + verbose_name = 'Custom Search' + + def ready(self): + import custom_search.receivers diff -r de4425ad62fb -r 7ab180ff6f7b custom_search/fields.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/custom_search/fields.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,29 @@ +"""Custom Haystack SearchFields.""" + +import haystack.fields + + +class MaxTermSizeCharField(haystack.fields.CharField): + """A CharField that discards large terms when preparing the search index. + + Some backends (e.g. Xapian) throw errors when terms are bigger than some + limit. This field omits the terms over a limit when preparing the data for + the search index. + + The keyword argument max_term_size sets the maximum size of a whitespace + delimited word/term. Terms over this size are not indexed. The default value + is 64. + """ + DEFAULT_MAX_TERM_SIZE = 64 + + def __init__(self, *args, **kwargs): + self.max_term_size = kwargs.pop('max_term_size', self.DEFAULT_MAX_TERM_SIZE) + super(MaxTermSizeCharField, self).__init__(*args, **kwargs) + + def prepare(self, obj): + text = super(MaxTermSizeCharField, self).prepare(obj) + if text is None or self.max_term_size is None: + return text + + terms = (term for term in text.split() if len(term) <= self.max_term_size) + return u' '.join(terms) diff -r de4425ad62fb -r 7ab180ff6f7b custom_search/forms.py --- a/custom_search/forms.py Wed May 13 20:27:17 2015 -0500 +++ b/custom_search/forms.py Wed May 13 20:29:23 2015 -0500 @@ -65,6 +65,12 @@ return self.cleaned_data + def clean_exact(self): + exact_field = self.cleaned_data['exact'] + if "'" in exact_field or '"' in exact_field: + raise forms.ValidationError("Quotes are not needed in this field") + return exact_field + def search(self): if not self.is_valid(): return self.no_query_found() @@ -83,24 +89,25 @@ self.cleaned_data['models'], username) - sqs = self.searchqueryset - # Note that in Haystack 2.x content is untrusted and is automatically # auto-escaped for us. # - # Filter on the q terms; these should be and'ed together: - terms = self.cleaned_data['q'].split() - for term in terms: - sqs = sqs.filter(content=term) + # Gather regular search terms + terms = ' '.join(self.cleaned_data['q'].split()) # Exact words or phrases: - if self.cleaned_data['exact']: - sqs = sqs.filter(content__exact=self.cleaned_data['exact']) + exact = self.cleaned_data['exact'].strip() + if exact: + exact = '"{}"'.format(exact) # Exclude terms: - terms = self.cleaned_data['exclude'].split() - for term in terms: - sqs = sqs.exclude(content=term) + exclude = ["-{}".format(term) for term in self.cleaned_data['exclude'].split()] + exclude = ' '.join(exclude) + + query = ' '.join([terms, exact, exclude]).strip() + logger.debug("auto_query: %s", query) + + sqs = self.searchqueryset.auto_query(query) if self.load_all: sqs = sqs.load_all() diff -r de4425ad62fb -r 7ab180ff6f7b custom_search/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/custom_search/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,134 @@ +"""This module contains a custom Haystack signal processing class to update the +search index in realtime. We update our search index by enqueuing edits and +deletes into a queue for batch processing. Our class ensures we only enqueue +content that should be in the search index. + +""" +from django.db.models import signals +import queued_search.signals +import haystack + +from bio.signals import profile_content_update +from forums.signals import topic_content_update, post_content_update + +import ygroup.models +from weblinks.models import Link +from podcast.models import Item +from news.models import Story +from downloads.models import Download +from forums.models import Forum, Topic, Post +from bio.models import UserProfile + + +UID = 'custom_search.signals' + + +class QueuedSignalProcessor(queued_search.signals.QueuedSignalProcessor): + """ + This customized version of queued_search's QueuedSignalProcessor + conditionally enqueues items to be indexed. + + """ + def __init__(self, *args, **kwargs): + + # We assume that it is okay to attempt to delete a model from the search + # index even if the model object is not in the index. In other words, + # attempting to delete an object from the index will not cause any + # errors if it is not in the index. Thus if we see an object that has an + # 'is_public' attribute, and it is false, we can safely enqueue a delete + # in case the 'is_public' attribute just went from True to False. We + # have no way of knowing that, it could have been False all along, but we + # just try the delete in case to be safe. + + # To make the code easier to read, use a table to drive our signal + # connecting and disconnecting: + self.signal_chain = [ + # Yahoo Group posts are always updated: + (signals.post_save, ygroup.models.Post, self.enqueue_save), + (signals.post_delete, ygroup.models.Post, self.enqueue_delete), + + # Weblink Links are updated if they are public: + (signals.post_save, Link, self.enqueue_public_save), + (signals.post_delete, Link, self.enqueue_delete), + + # Podcast Items are always updated: + (signals.post_save, Item, self.enqueue_save), + (signals.post_delete, Item, self.enqueue_delete), + + # News Stories are always updated: + (signals.post_save, Story, self.enqueue_save), + (signals.post_delete, Story, self.enqueue_delete), + + # Downloads are updated if they are public: + (signals.post_save, Download, self.enqueue_public_save), + (signals.post_delete, Download, self.enqueue_delete), + + # Forum Topics are updated if they belong to a public forum: + (topic_content_update, None, self.enqueue_topic_save), + (signals.post_delete, Topic, self.enqueue_delete), + + # Forum Posts are updated if they belong to a public forum: + (post_content_update, None, self.enqueue_post_save), + (signals.post_delete, Post, self.enqueue_delete), + + # UserProfiles are updated when we receive a special signal: + (profile_content_update, None, self.enqueue_profile), + (signals.post_delete, UserProfile, self.enqueue_delete), + ] + + super(QueuedSignalProcessor, self).__init__(*args, **kwargs) + + def setup(self): + """We override setup() so we can attach signal handlers to only the + models we search on. In some cases we have custom signals to tell us + when to update the search index. + + """ + for signal, sender, receiver in self.signal_chain: + signal.connect(receiver, sender=sender, dispatch_uid=UID) + + def teardown(self): + """Disconnect all signals we previously connected.""" + for signal, sender, receiver in self.signal_chain: + signal.disconnect(receiver, sender=sender, dispatch_uid=UID) + + def enqueue_public_save(self, sender, instance, **kwargs): + """Index only if the instance is_public. + + If not, enqueue a delete just in case the is_public flag got flipped + from True to False. + + """ + if instance.is_public: + self.enqueue_save(sender, instance, **kwargs) + else: + self.enqueue_delete(sender, instance, **kwargs) + + def enqueue_topic_save(self, sender, **kwargs): + """Enqueue only if the topic instance belongs to a public forum.""" + if sender.forum.id in Forum.objects.public_forum_ids(): + self.enqueue_save(Topic, sender, **kwargs) + + def enqueue_post_save(self, sender, **kwargs): + """Enqueue only if the post instance belongs to a public forum.""" + if sender.topic.forum.id in Forum.objects.public_forum_ids(): + self.enqueue_save(Post, sender, **kwargs) + + def enqueue_profile(self, sender, **kwargs): + """Forward the user profile instance on unconditionally.""" + self.enqueue_save(UserProfile, sender, **kwargs) + + +# Starting with Django 1.7, we'd see Django generate warnings if we defined +# a HAYSTACK_SIGNAL_PROCESSOR in our settings that referenced the class above. +# This is because Haystack creates an instance of our signal processor class +# (defined above) at import time, and thus imports this module very early in the +# application startup sequence. Warnings are then generated when this module +# imports our models, some of whose applications have not been imported yet. +# This problem will presumably go away when Haystack can fully support Django +# 1.7.x and implements an AppConfig with a ready() method. Until then, we don't +# use Haystack's signal processor object; we'll just create one here. This +# module will be imported when our custom_search app's ready() method runs. + +signal_processor = QueuedSignalProcessor(haystack.connections, + haystack.connection_router) diff -r de4425ad62fb -r 7ab180ff6f7b custom_search/signals.py --- a/custom_search/signals.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -"""This module contains a custom Haystack signal processing class to update the -search index in realtime. We update our search index by enqueuing edits and -deletes into a queue for batch processing. Our class ensures we only enqueue -content that should be in the search index. - -""" -from django.db.models import signals -import queued_search.signals - -from bio.signals import profile_content_update -from forums.signals import topic_content_update, post_content_update - -import ygroup.models -from weblinks.models import Link -from podcast.models import Item -from news.models import Story -from downloads.models import Download -from forums.models import Forum, Topic, Post -from bio.models import UserProfile - - -UID = 'custom_search.signals' - - -class QueuedSignalProcessor(queued_search.signals.QueuedSignalProcessor): - """ - This customized version of queued_search's QueuedSignalProcessor - conditionally enqueues items to be indexed. - - """ - def __init__(self, *args, **kwargs): - - # We assume that it is okay to attempt to delete a model from the search - # index even if the model object is not in the index. In other words, - # attempting to delete an object from the index will not cause any - # errors if it is not in the index. Thus if we see an object that has an - # 'is_public' attribute, and it is false, we can safely enqueue a delete - # in case the 'is_public' attribute just went from True to False. We - # have no way of knowing that, it could have been False all along, but we - # just try the delete in case to be safe. - - # To make the code easier to read, use a table to drive our signal - # connecting and disconnecting: - self.signal_chain = [ - # Yahoo Group posts are always updated: - (signals.post_save, ygroup.models.Post, self.enqueue_save), - (signals.post_delete, ygroup.models.Post, self.enqueue_delete), - - # Weblink Links are updated if they are public: - (signals.post_save, Link, self.enqueue_public_save), - (signals.post_delete, Link, self.enqueue_delete), - - # Podcast Items are always updated: - (signals.post_save, Item, self.enqueue_save), - (signals.post_delete, Item, self.enqueue_delete), - - # News Stories are always updated: - (signals.post_save, Story, self.enqueue_save), - (signals.post_delete, Story, self.enqueue_delete), - - # Downloads are updated if they are public: - (signals.post_save, Download, self.enqueue_public_save), - (signals.post_delete, Download, self.enqueue_delete), - - # Forum Topics are updated if they belong to a public forum: - (topic_content_update, None, self.enqueue_topic_save), - (signals.post_delete, Topic, self.enqueue_delete), - - # Forum Posts are updated if they belong to a public forum: - (post_content_update, None, self.enqueue_post_save), - (signals.post_delete, Post, self.enqueue_delete), - - # UserProfiles are updated when we receive a special signal: - (profile_content_update, None, self.enqueue_profile), - (signals.post_delete, UserProfile, self.enqueue_delete), - ] - - super(QueuedSignalProcessor, self).__init__(*args, **kwargs) - - def setup(self): - """We override setup() so we can attach signal handlers to only the - models we search on. In some cases we have custom signals to tell us - when to update the search index. - - """ - for signal, sender, receiver in self.signal_chain: - signal.connect(receiver, sender=sender, dispatch_uid=UID) - - def teardown(self): - """Disconnect all signals we previously connected.""" - for signal, sender, receiver in self.signal_chain: - signal.disconnect(receiver, sender=sender, dispatch_uid=UID) - - def enqueue_public_save(self, sender, instance, **kwargs): - """Index only if the instance is_public. - - If not, enqueue a delete just in case the is_public flag got flipped - from True to False. - - """ - if instance.is_public: - self.enqueue_save(sender, instance, **kwargs) - else: - self.enqueue_delete(sender, instance, **kwargs) - - def enqueue_topic_save(self, sender, **kwargs): - """Enqueue only if the topic instance belongs to a public forum.""" - if sender.forum.id in Forum.objects.public_forum_ids(): - self.enqueue_save(Topic, sender, **kwargs) - - def enqueue_post_save(self, sender, **kwargs): - """Enqueue only if the post instance belongs to a public forum.""" - if sender.topic.forum.id in Forum.objects.public_forum_ids(): - self.enqueue_save(Post, sender, **kwargs) - - def enqueue_profile(self, sender, **kwargs): - """Forward the user profile instance on unconditionally.""" - self.enqueue_save(UserProfile, sender, **kwargs) diff -r de4425ad62fb -r 7ab180ff6f7b donations/models.py --- a/donations/models.py Wed May 13 20:27:17 2015 -0500 +++ b/donations/models.py Wed May 13 20:29:23 2015 -0500 @@ -96,7 +96,7 @@ """Model to represent a donation to the website.""" user = models.ForeignKey(User, null=True, blank=True) - is_anonymous = models.BooleanField() + is_anonymous = models.BooleanField(default=False) test_ipn = models.BooleanField(default=False, verbose_name="Test IPN") txn_id = models.CharField(max_length=20, verbose_name="Txn ID") txn_type = models.CharField(max_length=64) diff -r de4425ad62fb -r 7ab180ff6f7b downloads/__init__.py --- a/downloads/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/downloads/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,1 +1,1 @@ -import signals +default_app_config = 'downloads.apps.DownloadsConfig' diff -r de4425ad62fb -r 7ab180ff6f7b downloads/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/downloads/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class DownloadsConfig(AppConfig): + name = 'downloads' + + def ready(self): + import downloads.receivers diff -r de4425ad62fb -r 7ab180ff6f7b downloads/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/downloads/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,63 @@ +"""Signal handlers for the downloads application. + +We use signals to compute the denormalized category counts whenever a download +is saved. + +""" +from django.db.models.signals import post_save +from django.db.models.signals import post_delete + +from downloads.models import Category, Download, PendingDownload + + +def on_download_save(sender, **kwargs): + """This function updates the count field for all categories. + It is called whenever a download is saved via a signal. + """ + if kwargs['created']: + # we only have to update the parent category + download = kwargs['instance'] + cat = download.category + cat.count = Download.public_objects.filter(category=cat).count() + cat.save() + else: + # update all categories just to be safe (an existing download could + # have been moved from one category to another + cats = Category.objects.all() + for cat in cats: + cat.count = Download.public_objects.filter(category=cat).count() + cat.save() + + +def on_download_delete(sender, **kwargs): + """This function updates the count field for the download's parent + category. It is called when a download is deleted via a signal. + + We now delete the uploaded file when the download is deleted. + """ + # update the parent category + download = kwargs['instance'] + cat = download.category + cat.count = Download.public_objects.filter(category=cat).count() + cat.save() + + # delete the actual file + if download.file: + download.file.delete(save=False) + + +def on_pending_download_delete(sender, **kwargs): + """Delete the uploaded file if it exists.""" + + download = kwargs['instance'] + # delete the actual file + if download.file: + download.file.delete(save=False) + + +post_save.connect(on_download_save, sender=Download, + dispatch_uid='downloads.receivers') +post_delete.connect(on_download_delete, sender=Download, + dispatch_uid='downloads.receivers') +post_delete.connect(on_pending_download_delete, sender=PendingDownload, + dispatch_uid='downloads.receivers') diff -r de4425ad62fb -r 7ab180ff6f7b downloads/search_indexes.py --- a/downloads/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/downloads/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -1,11 +1,12 @@ """Haystack search index for the downloads application.""" from haystack import indexes +from custom_search.fields import MaxTermSizeCharField from downloads.models import Download class DownloadIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) author = indexes.CharField(model_attr='user') pub_date = indexes.DateTimeField(model_attr='date_added') diff -r de4425ad62fb -r 7ab180ff6f7b downloads/signals.py --- a/downloads/signals.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,60 +0,0 @@ -"""Signals for the downloads application. -We use signals to compute the denormalized category counts whenever a download -is saved.""" -from django.db.models.signals import post_save -from django.db.models.signals import post_delete - -from downloads.models import Category, Download, PendingDownload - - -def on_download_save(sender, **kwargs): - """This function updates the count field for all categories. - It is called whenever a download is saved via a signal. - """ - if kwargs['created']: - # we only have to update the parent category - download = kwargs['instance'] - cat = download.category - cat.count = Download.public_objects.filter(category=cat).count() - cat.save() - else: - # update all categories just to be safe (an existing download could - # have been moved from one category to another - cats = Category.objects.all() - for cat in cats: - cat.count = Download.public_objects.filter(category=cat).count() - cat.save() - - -def on_download_delete(sender, **kwargs): - """This function updates the count field for the download's parent - category. It is called when a download is deleted via a signal. - - We now delete the uploaded file when the download is deleted. - """ - # update the parent category - download = kwargs['instance'] - cat = download.category - cat.count = Download.public_objects.filter(category=cat).count() - cat.save() - - # delete the actual file - if download.file: - download.file.delete(save=False) - - -def on_pending_download_delete(sender, **kwargs): - """Delete the uploaded file if it exists.""" - - download = kwargs['instance'] - # delete the actual file - if download.file: - download.file.delete(save=False) - - -post_save.connect(on_download_save, sender=Download, - dispatch_uid='downloads.signals') -post_delete.connect(on_download_delete, sender=Download, - dispatch_uid='downloads.signals') -post_delete.connect(on_pending_download_delete, sender=PendingDownload, - dispatch_uid='downloads.signals') diff -r de4425ad62fb -r 7ab180ff6f7b downloads/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/downloads/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,71 @@ +"""Tests for the downloads app signal handlers.""" + +from django.contrib.auth.models import User +from django.test import TestCase + +from mock import Mock + +import custom_search.receivers + +from downloads.models import Category +from downloads.models import Download +from downloads.models import PendingDownload + + +class ReceiverTestCase(TestCase): + + fixtures = ['downloads_categories.json'] + + def setUp(self): + self.user = User.objects.create_user('user', 'user@example.com', 'pw') + + # Don't let our custom search signal handler class catch any of the + # signals we are throwing here. + custom_search.receivers.signal_processor.teardown() + + def tearDown(self): + custom_search.receivers.signal_processor.setup() + + def test_download_signal_handlers(self): + + category = Category.objects.get(pk=1) + dl = Download(category=category, + title='Title', + description='Cool stuff', + is_public=True, + user=self.user, + ip_address='127.0.0.1') + dl.save() + + category = Category.objects.get(pk=1) + self.assertEqual(1, category.count) + + category2 = Category.objects.get(pk=4) + dl.category = category2 + dl.save() + + category = Category.objects.get(pk=1) + self.assertEqual(0, category.count) + category2 = Category.objects.get(pk=4) + self.assertEqual(1, category2.count) + + dl.delete() + category2 = Category.objects.get(pk=4) + self.assertEqual(0, category2.count) + + def test_pending_download_signal_handlers(self): + + category = Category.objects.get(pk=1) + dl = PendingDownload( + category=category, + title='Title', + description='Cool stuff', + user=self.user, + ip_address='127.0.0.1') + dl.save() + + dl.file = Mock() + dl.delete() + + dl.file.delete.assert_called_with(save=False) + diff -r de4425ad62fb -r 7ab180ff6f7b forums/__init__.py --- a/forums/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/forums/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,2 +1,1 @@ -import signals -import latest +default_app_config = 'forums.apps.ForumsConfig' diff -r de4425ad62fb -r 7ab180ff6f7b forums/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/forums/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ForumsConfig(AppConfig): + name = 'forums' + + def ready(self): + import forums.receivers + import forums.latest diff -r de4425ad62fb -r 7ab180ff6f7b forums/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/forums/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,60 @@ +""" +Signal handlers for the forums application. + +""" +from django.db.models.signals import post_save +from django.db.models.signals import post_delete + +from forums.models import Forum, Topic, Post +import forums.latest + + +def on_topic_save(sender, **kwargs): + if kwargs['created']: + topic = kwargs['instance'] + topic.forum.topic_count_update() + topic.forum.save() + + +def on_topic_delete(sender, **kwargs): + topic = kwargs['instance'] + topic.forum.topic_count_update() + topic.forum.save() + forums.latest.notify_topic_delete(topic) + + +def on_post_save(sender, **kwargs): + if kwargs['created']: + post = kwargs['instance'] + + # update the topic + post.topic.post_count_update() + post.topic.save() + + # update the forum + post.topic.forum.post_count_update() + post.topic.forum.save() + + +def on_post_delete(sender, **kwargs): + post = kwargs['instance'] + + # update the topic + try: + post.topic.post_count_update() + post.topic.save() + except Topic.DoesNotExist: + pass + else: + # update the forum + try: + post.topic.forum.post_count_update() + post.topic.forum.save() + except Forum.DoesNotExist: + pass + + +post_save.connect(on_topic_save, sender=Topic, dispatch_uid='forums.receivers') +post_delete.connect(on_topic_delete, sender=Topic, dispatch_uid='forums.receivers') +post_save.connect(on_post_save, sender=Post, dispatch_uid='forums.receivers') +post_delete.connect(on_post_delete, sender=Post, dispatch_uid='forums.receivers') diff -r de4425ad62fb -r 7ab180ff6f7b forums/search_indexes.py --- a/forums/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/forums/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -1,6 +1,7 @@ """Haystack search index for the weblinks application.""" from haystack import indexes +from custom_search.fields import MaxTermSizeCharField from forums.models import Forum, Topic, Post @@ -20,7 +21,7 @@ class PostIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) author = indexes.CharField(model_attr='user') pub_date = indexes.DateTimeField(model_attr='creation_date') diff -r de4425ad62fb -r 7ab180ff6f7b forums/signals.py --- a/forums/signals.py Wed May 13 20:27:17 2015 -0500 +++ b/forums/signals.py Wed May 13 20:29:23 2015 -0500 @@ -1,68 +1,10 @@ """ -Signal handlers & signals for the forums application. +Signals for the forums application. """ -from django.db.models.signals import post_save -from django.db.models.signals import post_delete import django.dispatch -from forums.models import Forum, Topic, Post - -def on_topic_save(sender, **kwargs): - if kwargs['created']: - topic = kwargs['instance'] - topic.forum.topic_count_update() - topic.forum.save() - - -def on_topic_delete(sender, **kwargs): - topic = kwargs['instance'] - topic.forum.topic_count_update() - topic.forum.save() - forums.latest.notify_topic_delete(topic) - - -def on_post_save(sender, **kwargs): - if kwargs['created']: - post = kwargs['instance'] - - # update the topic - post.topic.post_count_update() - post.topic.save() - - # update the forum - post.topic.forum.post_count_update() - post.topic.forum.save() - - -def on_post_delete(sender, **kwargs): - post = kwargs['instance'] - - # update the topic - try: - post.topic.post_count_update() - post.topic.save() - except Topic.DoesNotExist: - pass - else: - # update the forum - try: - post.topic.forum.post_count_update() - post.topic.forum.save() - except Forum.DoesNotExist: - pass - - -post_save.connect(on_topic_save, sender=Topic, dispatch_uid='forums.signals') -post_delete.connect(on_topic_delete, sender=Topic, dispatch_uid='forums.signals') - -post_save.connect(on_post_save, sender=Post, dispatch_uid='forums.signals') -post_delete.connect(on_post_delete, sender=Post, dispatch_uid='forums.signals') - - -# Signals for the forums application. -# # This signal is sent when a topic has had its textual content (title) changed. # The provided arguments are: # sender - the topic model instance @@ -108,7 +50,3 @@ """ post_content_update.send_robust(post, created=False) - - -# Avoid circular imports -import forums.latest diff -r de4425ad62fb -r 7ab180ff6f7b forums/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/forums/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,63 @@ +"""Tests for the forum app's signal handlers.""" + +from django.contrib.auth.models import User +from django.test import TestCase + +from mock import patch + +import custom_search.receivers + +from forums.models import Forum +from forums.models import Post +from forums.models import Topic + + +class ReceiverTestCase(TestCase): + + fixtures = ['forums.json'] + + def setUp(self): + self.user = User.objects.create_user('user', 'user@example.com', 'pw') + + # Don't let our custom search signal handler class catch any of the + # signals we are throwing here. + custom_search.receivers.signal_processor.teardown() + + def tearDown(self): + custom_search.receivers.signal_processor.setup() + + @patch('forums.latest.notify_topic_delete') + def test_signal_handlers(self, mock_latest): + forum = Forum.objects.get(pk=2) + topic = Topic(forum=forum, + name='Test Topic', + user=self.user) + topic.save() + forum = Forum.objects.get(pk=2) + self.assertEqual(1, forum.topic_count) + + post = Post(topic=topic, user=self.user, body='test') + post.save() + + topic = Topic.objects.get(pk=topic.pk) + self.assertEqual(topic.post_count, 1) + forum = Forum.objects.get(pk=2) + self.assertEqual(forum.post_count, 1) + + # To prevent cascading deletes + topic.last_post_pre_delete() + topic.save() + forum.last_post_pre_delete() + forum.save() + + post.delete() + topic = Topic.objects.get(pk=topic.pk) + self.assertEqual(topic.post_count, 0) + forum = Forum.objects.get(pk=2) + self.assertEqual(forum.post_count, 0) + + topic.delete() + forum = Forum.objects.get(pk=2) + self.assertEqual(0, forum.topic_count) + + mock_latest.assert_called_once_with(topic) diff -r de4425ad62fb -r 7ab180ff6f7b gcalendar/calendar.py --- a/gcalendar/calendar.py Wed May 13 20:27:17 2015 -0500 +++ b/gcalendar/calendar.py Wed May 13 20:29:23 2015 -0500 @@ -119,9 +119,9 @@ def _make_time(self, date, time=None, tz_name=None): """ - Returns the gdata formatted date/time string given a date, optional time, - and optional time zone name (e.g. 'US/Pacific'). If the time zone name is None, - no time zone info will be added to the string. + Returns the formatted date/time string given a date, optional time, and + optional time zone name (e.g. 'US/Pacific'). If the time zone name is + None, no time zone info will be added to the string. """ if time: diff -r de4425ad62fb -r 7ab180ff6f7b messages/__init__.py --- a/messages/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/messages/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,1 +1,2 @@ +default_app_config = 'messages.apps.MessagesConfig' MSG_BOX_LIMIT = 30 # hard limit on # of msgs per box diff -r de4425ad62fb -r 7ab180ff6f7b messages/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/messages/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MessagesConfig(AppConfig): + name = 'messages' + verbose_name = 'Private Messages' diff -r de4425ad62fb -r 7ab180ff6f7b news/search_indexes.py --- a/news/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/news/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -1,11 +1,12 @@ """Haystack search index for the news application.""" from haystack import indexes +from custom_search.fields import MaxTermSizeCharField from news.models import Story class StoryIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) author = indexes.CharField(model_attr='submitter') pub_date = indexes.DateTimeField(model_attr='date_submitted') diff -r de4425ad62fb -r 7ab180ff6f7b podcast/search_indexes.py --- a/podcast/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/podcast/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -1,11 +1,12 @@ """Haystack search index for the news application.""" from haystack import indexes +from custom_search.fields import MaxTermSizeCharField from podcast.models import Item class ItemIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) author = indexes.CharField(model_attr='author') pub_date = indexes.DateTimeField(model_attr='pubdate') diff -r de4425ad62fb -r 7ab180ff6f7b potd/__init__.py --- a/potd/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/potd/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,1 +1,1 @@ -import signals +default_app_config = 'potd.apps.PotdConfig' diff -r de4425ad62fb -r 7ab180ff6f7b potd/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/potd/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class PotdConfig(AppConfig): + name = 'potd' + verbose_name = 'Photo of the Day' + label = 'potd' + + def ready(self): + import potd.receivers diff -r de4425ad62fb -r 7ab180ff6f7b potd/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/potd/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,31 @@ +""" +Signal handlers for the potd application. + +""" +from django.db.models.signals import post_save, post_delete + +from potd.models import Photo, Sequence + + +def on_photo_save(sender, **kwargs): + """ + This function is executed when a Photo is saved. It inserts the photo into + the current sequence. + + """ + photo = kwargs['instance'] + Sequence.objects.insert_photo(photo.pk) + + +def on_photo_delete(sender, **kwargs): + """ + This function is executed when a Photo is deleted. It removes the photo from + the current sequence of photos. + + """ + photo = kwargs['instance'] + Sequence.objects.remove_photo(photo.pk) + + +post_save.connect(on_photo_save, sender=Photo, dispatch_uid='potd.receivers') +post_delete.connect(on_photo_delete, sender=Photo, dispatch_uid='potd.receivers') diff -r de4425ad62fb -r 7ab180ff6f7b potd/signals.py --- a/potd/signals.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -""" -Signal handlers for the potd application. - -""" -from django.db.models.signals import post_save, post_delete - -from potd.models import Photo, Sequence - - -def on_photo_save(sender, **kwargs): - """ - This function is executed when a Photo is saved. It inserts the photo into - the current sequence. - - """ - photo = kwargs['instance'] - Sequence.objects.insert_photo(photo.pk) - - -def on_photo_delete(sender, **kwargs): - """ - This function is executed when a Photo is deleted. It removes the photo from - the current sequence of photos. - - """ - photo = kwargs['instance'] - Sequence.objects.remove_photo(photo.pk) - - -post_save.connect(on_photo_save, sender=Photo, dispatch_uid='potd.signals') -post_delete.connect(on_photo_delete, sender=Photo, dispatch_uid='potd.signals') diff -r de4425ad62fb -r 7ab180ff6f7b potd/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/potd/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,34 @@ +"""Tests for potd signal handlers.""" +from django.contrib.auth.models import User +from django.test import TestCase + +from mock import Mock + +from potd.models import Photo, Sequence, Current + + +class PotdSignalRcvrTestCase(TestCase): + + fixtures = ['potd_test.json'] + + def test_on_photo_save(self): + user = User.objects.get(pk=1) + photo = Photo(photo='/tmp/1.jpg', + caption='caption', + description='desc', + user=user) + photo.generate_thumb = Mock() + photo.save() + + current = Current.objects.get_current_id() + self.assertTrue(current != photo.pk) + + seq = Sequence.objects.get(pk=1) + expected = '1,{},2,3'.format(photo.pk) + self.assertEqual(seq.seq, expected) + + def test_on_photo_delete(self): + photo = Photo.objects.get(pk=2) + photo.delete() + seq = Sequence.objects.get(pk=1) + self.assertEqual(seq.seq, '1,3') diff -r de4425ad62fb -r 7ab180ff6f7b requirements_dev.txt --- a/requirements_dev.txt Wed May 13 20:27:17 2015 -0500 +++ b/requirements_dev.txt Wed May 13 20:29:23 2015 -0500 @@ -1,7 +1,7 @@ -Django==1.6.6 +Django==1.7.7 Markdown==2.5.1 MySQL-python==1.2.5 -django-debug-toolbar==1.0 +django-debug-toolbar==1.3.0 -e git+https://github.com/gremmie/django-elsewhere.git@1203bd331aba4c5d4e702cc4e64d807310f2b591#egg=django_elsewhere-master django-haystack==2.1.0 django-tagging==0.3.1 @@ -28,7 +28,7 @@ ftfy==2.0.1 Pillow==2.7.0 boto==2.13.0 -sqlparse==0.1.10 +sqlparse==0.1.14 billiard==3.3.0.13 google-api-python-client==1.3.1 httplib2==0.9 @@ -39,6 +39,8 @@ simplejson==3.6.5 uritemplate==0.6 mock==1.0.1 +lxml==3.4.2 +testfixtures==4.1.2 # # These packages I punted on and hacked into my virtualenv by # symlinking to the global site-packages: diff -r de4425ad62fb -r 7ab180ff6f7b sg101/apache/sg101.wsgi --- a/sg101/apache/sg101.wsgi Wed May 13 20:27:17 2015 -0500 +++ b/sg101/apache/sg101.wsgi Wed May 13 20:29:23 2015 -0500 @@ -35,8 +35,8 @@ if not OFFLINE: os.environ['DJANGO_SETTINGS_MODULE'] = 'sg101.settings.production' - import django.core.handlers.wsgi - application = django.core.handlers.wsgi.WSGIHandler() + from django.core.wsgi import get_wsgi_application + application = get_wsgi_application() else: application = offline_handler diff -r de4425ad62fb -r 7ab180ff6f7b sg101/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sg101/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,8 @@ +from django.contrib.messages.apps import MessagesConfig + + +# The Django messages app conflicts with our own messages app. +# Override the label to fix this. + +class DjangoMessagesConfig(MessagesConfig): + label = 'django_messages' diff -r de4425ad62fb -r 7ab180ff6f7b sg101/settings/base.py --- a/sg101/settings/base.py Wed May 13 20:27:17 2015 -0500 +++ b/sg101/settings/base.py Wed May 13 20:29:23 2015 -0500 @@ -103,7 +103,7 @@ 'django.contrib.contenttypes', 'django.contrib.flatpages', 'django.contrib.humanize', - 'django.contrib.messages', + 'sg101.apps.DjangoMessagesConfig', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.staticfiles', @@ -190,8 +190,6 @@ ####################################################################### # Haystack Search Settings ####################################################################### -HAYSTACK_SIGNAL_PROCESSOR = 'custom_search.signals.QueuedSignalProcessor' - HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'xapian_backend.XapianEngine', @@ -350,3 +348,6 @@ ], } + +# Turn off warning about test runner behavior change +SILENCED_SYSTEM_CHECKS = ['1_6.W001'] diff -r de4425ad62fb -r 7ab180ff6f7b sg101/settings/local.py --- a/sg101/settings/local.py Wed May 13 20:27:17 2015 -0500 +++ b/sg101/settings/local.py Wed May 13 20:29:23 2015 -0500 @@ -32,7 +32,8 @@ 'debug_toolbar.middleware.DebugToolbarMiddleware') INSTALLED_APPS.append('debug_toolbar') DEBUG_TOOLBAR_CONFIG = { - 'INTERCEPT_REDIRECTS': True, + 'DISABLE_PANELS': set(), + 'JQUERY_URL': '', } # Logging configuration diff -r de4425ad62fb -r 7ab180ff6f7b sg101/templates/core/admin_dashboard.html --- a/sg101/templates/core/admin_dashboard.html Wed May 13 20:27:17 2015 -0500 +++ b/sg101/templates/core/admin_dashboard.html Wed May 13 20:29:23 2015 -0500 @@ -1,5 +1,6 @@ {% if user.is_staff %} {% if flagged_posts or flagged_comments or flagged_profiles or event_requests or new_stories or new_downloads or new_links or flagged_shouts or broken_links or flagged_msgs or new_bands %} +
+
{% endif %} {% endif %} diff -r de4425ad62fb -r 7ab180ff6f7b sg101/urls.py --- a/sg101/urls.py Wed May 13 20:27:17 2015 -0500 +++ b/sg101/urls.py Wed May 13 20:29:23 2015 -0500 @@ -15,8 +15,6 @@ from core.views import FixedView -admin.autodiscover() - urlpatterns = patterns('', url(r'^$', TemplateView.as_view(template_name='home.html'), diff -r de4425ad62fb -r 7ab180ff6f7b shoutbox/models.py --- a/shoutbox/models.py Wed May 13 20:27:17 2015 -0500 +++ b/shoutbox/models.py Wed May 13 20:29:23 2015 -0500 @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.utils.html import escape, urlize -from smiley import smilify_html +from smiley.utils import smilify_html class Shout(models.Model): diff -r de4425ad62fb -r 7ab180ff6f7b smiley/__init__.py --- a/smiley/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/smiley/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,70 +0,0 @@ -""" -Smiley classes and functions. - -""" -from django.utils.safestring import SafeData -from django.utils.html import conditional_escape - -from smiley.models import Smiley - - -class SmilifyHtml(object): - """ - A class to "smilify" text by replacing text with HTML img tags for smiley - images. - """ - def __init__(self): - self.map = Smiley.objects.get_smiley_map() - - def convert(self, value, autoescape=False): - """ - Converts and returns the supplied text with the HTML version of the - smileys. - """ - if not value: - return u'' - - if not autoescape or isinstance(value, SafeData): - esc = lambda x: x - else: - esc = conditional_escape - - words = value.split() - for i, word in enumerate(words): - if word in self.map: - words[i] = self.map[word] - else: - words[i] = esc(words[i]) - return u' '.join(words) - - -class SmilifyMarkdown(object): - """ - A class to "smilify" text by replacing text with Markdown image syntax for - smiley images. - """ - def __init__(self, relative_urls=True): - self.regexes = Smiley.objects.get_smiley_regexes( - relative_urls=relative_urls) - - def convert(self, s): - """ - Returns a string copy of the input s that has the smiley codes replaced - with Markdown for smiley images. - """ - if not s: - return u'' - - for regex, repl in self.regexes: - s = regex.sub(repl, s) - return s - - -def smilify_html(value, autoescape=False): - """ - A convenience function to "smilify" text by replacing text with HTML - img tags of smilies. - """ - s = SmilifyHtml() - return s.convert(value, autoescape=autoescape) - diff -r de4425ad62fb -r 7ab180ff6f7b smiley/models.py --- a/smiley/models.py Wed May 13 20:27:17 2015 -0500 +++ b/smiley/models.py Wed May 13 20:29:23 2015 -0500 @@ -64,7 +64,7 @@ image = models.ImageField(upload_to='smiley/images/') title = models.CharField(max_length=32) code = models.CharField(max_length=32) - is_extra = models.BooleanField() + is_extra = models.BooleanField(default=False) objects = SmileyManager() diff -r de4425ad62fb -r 7ab180ff6f7b smiley/templatetags/smiley_tags.py --- a/smiley/templatetags/smiley_tags.py Wed May 13 20:27:17 2015 -0500 +++ b/smiley/templatetags/smiley_tags.py Wed May 13 20:29:23 2015 -0500 @@ -1,11 +1,12 @@ """ -Template tags for the smiley application. +Template tags for the smiley application. """ from django import template from django.template.defaultfilters import stringfilter from django.utils.safestring import mark_safe from smiley.models import Smiley +from smiley.utils import smilify_html register = template.Library() @@ -14,7 +15,6 @@ @stringfilter def smiley_html(value, autoescape=False): """A filter to "smilify" text by replacing text with HTML img tags of smilies.""" - from smiley import smilify_html return mark_safe(smilify_html(value, autoescape=autoescape)) smiley_html.needs_autoescape = True diff -r de4425ad62fb -r 7ab180ff6f7b smiley/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/smiley/utils.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,69 @@ +""" +Smiley classes and functions. + +""" +from django.utils.safestring import SafeData +from django.utils.html import conditional_escape + +from models import Smiley + + +class SmilifyHtml(object): + """ + A class to "smilify" text by replacing text with HTML img tags for smiley + images. + """ + def __init__(self): + self.map = Smiley.objects.get_smiley_map() + + def convert(self, value, autoescape=False): + """ + Converts and returns the supplied text with the HTML version of the + smileys. + """ + if not value: + return u'' + + if not autoescape or isinstance(value, SafeData): + esc = lambda x: x + else: + esc = conditional_escape + + words = value.split() + for i, word in enumerate(words): + if word in self.map: + words[i] = self.map[word] + else: + words[i] = esc(words[i]) + return u' '.join(words) + + +class SmilifyMarkdown(object): + """ + A class to "smilify" text by replacing text with Markdown image syntax for + smiley images. + """ + def __init__(self, relative_urls=True): + self.regexes = Smiley.objects.get_smiley_regexes( + relative_urls=relative_urls) + + def convert(self, s): + """ + Returns a string copy of the input s that has the smiley codes replaced + with Markdown for smiley images. + """ + if not s: + return u'' + + for regex, repl in self.regexes: + s = regex.sub(repl, s) + return s + + +def smilify_html(value, autoescape=False): + """ + A convenience function to "smilify" text by replacing text with HTML + img tags of smilies. + """ + s = SmilifyHtml() + return s.convert(value, autoescape=autoescape) diff -r de4425ad62fb -r 7ab180ff6f7b weblinks/__init__.py --- a/weblinks/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/weblinks/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -1,1 +1,1 @@ -import signals +default_app_config = 'weblinks.apps.WeblinksConfig' diff -r de4425ad62fb -r 7ab180ff6f7b weblinks/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weblinks/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class WeblinksConfig(AppConfig): + name = 'weblinks' + verbose_name = 'Web Links' + + def ready(self): + import weblinks.receivers diff -r de4425ad62fb -r 7ab180ff6f7b weblinks/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weblinks/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,43 @@ +"""Signal handlers for the weblinks application. + +We use signals to compute the denormalized category counts whenever a weblink +is saved. +""" +from django.db.models.signals import post_save +from django.db.models.signals import post_delete + +from weblinks.models import Category, Link + + +def on_link_save(sender, **kwargs): + """This function updates the count field for all categories. + It is called whenever a link is saved via a signal. + """ + if kwargs['created']: + # we only have to update the parent category + link = kwargs['instance'] + cat = link.category + cat.count = Link.public_objects.filter(category=cat).count() + cat.save() + else: + # update all categories just to be safe (an existing link could + # have been moved from one category to another + cats = Category.objects.all() + for cat in cats: + cat.count = Link.public_objects.filter(category=cat).count() + cat.save() + + +def on_link_delete(sender, **kwargs): + """This function updates the count field for the link's parent + category. It is called when a link is deleted via a signal. + """ + # update the parent category + link = kwargs['instance'] + cat = link.category + cat.count = Link.public_objects.filter(category=cat).count() + cat.save() + + +post_save.connect(on_link_save, sender=Link, dispatch_uid='weblinks.receivers') +post_delete.connect(on_link_delete, sender=Link, dispatch_uid='weblinks.receivers') diff -r de4425ad62fb -r 7ab180ff6f7b weblinks/search_indexes.py --- a/weblinks/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/weblinks/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -1,11 +1,12 @@ """Haystack search index for the weblinks application.""" from haystack import indexes +from custom_search.fields import MaxTermSizeCharField from weblinks.models import Link class LinkIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) author = indexes.CharField(model_attr='user') pub_date = indexes.DateTimeField(model_attr='date_added') diff -r de4425ad62fb -r 7ab180ff6f7b weblinks/signals.py --- a/weblinks/signals.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -"""Signals for the weblinks application. -We use signals to compute the denormalized category counts whenever a weblink -is saved.""" -from django.db.models.signals import post_save -from django.db.models.signals import post_delete - -from weblinks.models import Category, Link - - -def on_link_save(sender, **kwargs): - """This function updates the count field for all categories. - It is called whenever a link is saved via a signal. - """ - if kwargs['created']: - # we only have to update the parent category - link = kwargs['instance'] - cat = link.category - cat.count = Link.public_objects.filter(category=cat).count() - cat.save() - else: - # update all categories just to be safe (an existing link could - # have been moved from one category to another - cats = Category.objects.all() - for cat in cats: - cat.count = Link.public_objects.filter(category=cat).count() - cat.save() - - -def on_link_delete(sender, **kwargs): - """This function updates the count field for the link's parent - category. It is called when a link is deleted via a signal. - """ - # update the parent category - link = kwargs['instance'] - cat = link.category - cat.count = Link.public_objects.filter(category=cat).count() - cat.save() - - -post_save.connect(on_link_save, sender=Link, dispatch_uid='weblinks.signals') -post_delete.connect(on_link_delete, sender=Link, dispatch_uid='weblinks.signals') diff -r de4425ad62fb -r 7ab180ff6f7b weblinks/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weblinks/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,51 @@ +"""Tests for the weblink app signal handlers.""" + +from django.contrib.auth.models import User +from django.test import TestCase + +import custom_search.receivers + +from weblinks.models import Category +from weblinks.models import Link + + +class ReceiverTestCase(TestCase): + + fixtures = ['weblinks_categories.json'] + + def setUp(self): + self.user = User.objects.create_user('user', 'user@example.com', 'pw') + + # Don't let our custom search signal handler class catch any of the + # signals we are throwing here. + custom_search.receivers.signal_processor.teardown() + + def tearDown(self): + custom_search.receivers.signal_processor.setup() + + def test_signal_handlers(self): + + category = Category.objects.get(pk=1) + link = Link(category=category, + title='Title', + url='http://example.com/', + description='Cool stuff', + is_public=True, + user=self.user) + link.save() + + category = Category.objects.get(pk=1) + self.assertEqual(1, category.count) + + category2 = Category.objects.get(pk=4) + link.category = category2 + link.save() + + category = Category.objects.get(pk=1) + self.assertEqual(0, category.count) + category2 = Category.objects.get(pk=4) + self.assertEqual(1, category2.count) + + link.delete() + category2 = Category.objects.get(pk=4) + self.assertEqual(0, category2.count) diff -r de4425ad62fb -r 7ab180ff6f7b wiki/__init__.py --- a/wiki/__init__.py Wed May 13 20:27:17 2015 -0500 +++ b/wiki/__init__.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,1 @@ +default_app_config = 'wiki.apps.WikiConfig' diff -r de4425ad62fb -r 7ab180ff6f7b wiki/apps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wiki/apps.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class WikiConfig(AppConfig): + name = 'wiki' + + def ready(self): + import wiki.receivers diff -r de4425ad62fb -r 7ab180ff6f7b wiki/models.py --- a/wiki/models.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -"""The wiki application integrates an external Wiki app with our Django -application. - -The wiki application has no models. It consists of some signals and -middleware only. - -""" -import wiki.signals diff -r de4425ad62fb -r 7ab180ff6f7b wiki/receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wiki/receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,46 @@ +"""Signal handlers for wiki integration. + +We are interested in hearing about users logging in and out, so we can create +and destroy an external cookie to allow access to the wiki. + +""" +import logging + +from django.contrib.auth.signals import user_logged_in, user_logged_out + +from wiki.constants import SESSION_SET_MEMBER + +logger = logging.getLogger(__name__) + + +def login_callback(sender, request, user, **kwargs): + """Signal callback function for a user logging in. + + Sets a flag for the middleware to create an external cookie. + + """ + logger.info('User login: %s', user.username) + + request.wiki_set_cookie = True + + +def logout_callback(sender, request, user, **kwargs): + """Signal callback function for a user logging in. + + Sets a flag for the middleware to delete the external cookie. + + Since the user is about to logout, her session will be wiped out after + this function returns. This forces us to set an attribute on the request + object so that the response middleware can delete the wiki's cookie. + + """ + if user: + logger.info('User logout: %s', user.username) + + # Remember what Redis set member to delete by adding an attribute to the + # request object: + request.wiki_delete_cookie = request.session.get(SESSION_SET_MEMBER) + + +user_logged_in.connect(login_callback, dispatch_uid='wiki.receivers.login') +user_logged_out.connect(logout_callback, dispatch_uid='wiki.receivers.logout') diff -r de4425ad62fb -r 7ab180ff6f7b wiki/signals.py --- a/wiki/signals.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -"""Signal handlers for wiki integration. - -We are interested in hearing about users logging in and out, so we can create -and destroy an external cookie to allow access to the wiki. - -""" -import logging - -from django.contrib.auth.signals import user_logged_in, user_logged_out - -from wiki.constants import SESSION_SET_MEMBER - -logger = logging.getLogger(__name__) - - -def login_callback(sender, request, user, **kwargs): - """Signal callback function for a user logging in. - - Sets a flag for the middleware to create an external cookie. - - """ - logger.info('User login: %s', user.username) - - request.wiki_set_cookie = True - - -def logout_callback(sender, request, user, **kwargs): - """Signal callback function for a user logging in. - - Sets a flag for the middleware to delete the external cookie. - - Since the user is about to logout, her session will be wiped out after - this function returns. This forces us to set an attribute on the request - object so that the response middleware can delete the wiki's cookie. - - """ - if user: - logger.info('User logout: %s', user.username) - - # Remember what Redis set member to delete by adding an attribute to the - # request object: - request.wiki_delete_cookie = request.session.get(SESSION_SET_MEMBER) - - -user_logged_in.connect(login_callback, dispatch_uid='wiki.signals.login') -user_logged_out.connect(logout_callback, dispatch_uid='wiki.signals.logout') diff -r de4425ad62fb -r 7ab180ff6f7b wiki/tests.py --- a/wiki/tests.py Wed May 13 20:27:17 2015 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -""" -Tests for the wiki integration application. - -""" -import hashlib -import datetime - -from django.contrib.auth.models import User -from django.test import TestCase -from django.test.client import RequestFactory -from django.http import HttpResponse -from django.conf import settings - -from core.services import get_redis_connection -from wiki.middleware import WikiMiddleware -from wiki.constants import SESSION_SET_MEMBER - - -class MiddleWareTestCase(TestCase): - - def setUp(self): - self.factory = RequestFactory() - self.user = User.objects.create_user('test_user', 'test@example.com', - 'password') - self.conn = get_redis_connection() - self.mw = WikiMiddleware() - - def tearDown(self): - self.conn.delete(settings.WIKI_REDIS_SET) - - def create_request(self): - request = self.factory.get('/contact/') - request.session = {} - request.user = self.user - return request - - def test_middleware(self): - - request = self.create_request() - response = HttpResponse() - - request.wiki_set_cookie = True - response = self.mw.process_response(request, response) - - cookie = response.cookies.get(settings.WIKI_COOKIE_NAME) - cookie_val = '' - self.assertIsNotNone(cookie) - if cookie: - self.assertEqual(cookie['domain'], settings.WIKI_COOKIE_DOMAIN) - self.assertEqual(cookie['path'], '/') - self.assertEqual(cookie['max-age'], settings.WIKI_COOKIE_AGE) - - cookie_val = cookie.value - try: - user, email, key = cookie_val.split('#') - except ValueError: - self.fail('invalid cookie value') - else: - self.assertEqual(user, self.user.username) - self.assertEqual(email, self.user.email) - self.assertEqual(len(key), 64) - - self.assertEqual(self.conn.zcard(settings.WIKI_REDIS_SET), 1) - - h = hashlib.sha256() - h.update(cookie_val) - member = h.hexdigest() - - score = self.conn.zscore(settings.WIKI_REDIS_SET, member) - now = datetime.datetime.utcnow() - session_start = datetime.datetime.fromtimestamp(score) - self.assertLess(now - session_start, datetime.timedelta(seconds=2)) - - session_member = request.session.get(SESSION_SET_MEMBER) - self.assertTrue(session_member and session_member == member) - - # test the destroy session logic - - request = self.create_request() - request.wiki_delete_cookie = member - response = self.mw.process_response(request, response) - - cookie = response.cookies.get(settings.WIKI_COOKIE_NAME) - self.assertIsNotNone(cookie) - if cookie: - self.assertEqual(cookie.value, '') - self.assertEqual(cookie['domain'], settings.WIKI_COOKIE_DOMAIN) - self.assertEqual(cookie['path'], '/') - self.assertEqual(cookie['max-age'], 0) - self.assertEqual(cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT') - - self.assertEqual(self.conn.zcard(settings.WIKI_REDIS_SET), 0) diff -r de4425ad62fb -r 7ab180ff6f7b wiki/tests/test_middleware.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wiki/tests/test_middleware.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,92 @@ +""" +Tests for the wiki integration application. + +""" +import hashlib +import datetime + +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.client import RequestFactory +from django.http import HttpResponse +from django.conf import settings + +from core.services import get_redis_connection +from wiki.middleware import WikiMiddleware +from wiki.constants import SESSION_SET_MEMBER + + +class MiddleWareTestCase(TestCase): + + def setUp(self): + self.factory = RequestFactory() + self.user = User.objects.create_user('test_user', 'test@example.com', + 'password') + self.conn = get_redis_connection() + self.mw = WikiMiddleware() + + def tearDown(self): + self.conn.delete(settings.WIKI_REDIS_SET) + + def create_request(self): + request = self.factory.get('/contact/') + request.session = {} + request.user = self.user + return request + + def test_middleware(self): + + request = self.create_request() + response = HttpResponse() + + request.wiki_set_cookie = True + response = self.mw.process_response(request, response) + + cookie = response.cookies.get(settings.WIKI_COOKIE_NAME) + cookie_val = '' + self.assertIsNotNone(cookie) + if cookie: + self.assertEqual(cookie['domain'], settings.WIKI_COOKIE_DOMAIN) + self.assertEqual(cookie['path'], '/') + self.assertEqual(cookie['max-age'], settings.WIKI_COOKIE_AGE) + + cookie_val = cookie.value + try: + user, email, key = cookie_val.split('#') + except ValueError: + self.fail('invalid cookie value') + else: + self.assertEqual(user, self.user.username) + self.assertEqual(email, self.user.email) + self.assertEqual(len(key), 64) + + self.assertEqual(self.conn.zcard(settings.WIKI_REDIS_SET), 1) + + h = hashlib.sha256() + h.update(cookie_val) + member = h.hexdigest() + + score = self.conn.zscore(settings.WIKI_REDIS_SET, member) + now = datetime.datetime.utcnow() + session_start = datetime.datetime.fromtimestamp(score) + self.assertLess(now - session_start, datetime.timedelta(seconds=2)) + + session_member = request.session.get(SESSION_SET_MEMBER) + self.assertTrue(session_member and session_member == member) + + # test the destroy session logic + + request = self.create_request() + request.wiki_delete_cookie = member + response = self.mw.process_response(request, response) + + cookie = response.cookies.get(settings.WIKI_COOKIE_NAME) + self.assertIsNotNone(cookie) + if cookie: + self.assertEqual(cookie.value, '') + self.assertEqual(cookie['domain'], settings.WIKI_COOKIE_DOMAIN) + self.assertEqual(cookie['path'], '/') + self.assertEqual(cookie['max-age'], 0) + self.assertEqual(cookie['expires'], 'Thu, 01-Jan-1970 00:00:00 GMT') + + self.assertEqual(self.conn.zcard(settings.WIKI_REDIS_SET), 0) diff -r de4425ad62fb -r 7ab180ff6f7b wiki/tests/test_receivers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wiki/tests/test_receivers.py Wed May 13 20:29:23 2015 -0500 @@ -0,0 +1,32 @@ +"""Tests for the wiki app's signal handlers.""" +import logging + +from django.contrib.auth.models import User +from django.test import TestCase + +from testfixtures import log_capture + + +class ReceiverTestCase(TestCase): + + def setUp(self): + self.user = User.objects.create_user('user', 'user@example.com', 'pw') + + # Temporarily enable logging + self.old_disable = logging.getLogger().manager.disable + logging.disable(logging.NOTSET) + + def tearDown(self): + logging.disable(self.old_disable) + + @log_capture('wiki.receivers') + def test_signal_handlers(self, lc): + # We don't have access to the dummy request that the test client creates + # when logging in, so we can't really check to see if we added + # attributes to the request object. But that code is pretty simple, so + # lets just test that we logged something so we know our signal handlers + # are hooked up and running. + self.client.login(username='user', password='pw') + self.client.logout() + lc.check(('wiki.receivers', 'INFO', 'User login: user'), + ('wiki.receivers', 'INFO', 'User logout: user')) diff -r de4425ad62fb -r 7ab180ff6f7b ygroup/search_indexes.py --- a/ygroup/search_indexes.py Wed May 13 20:27:17 2015 -0500 +++ b/ygroup/search_indexes.py Wed May 13 20:29:23 2015 -0500 @@ -4,11 +4,12 @@ """ from haystack import indexes +from custom_search.fields import MaxTermSizeCharField from ygroup.models import Post class PostIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) + text = MaxTermSizeCharField(document=True, use_template=True) pub_date = indexes.DateTimeField(model_attr='creation_date') def get_model(self):