diff --git a/lms/djangoapps/rss_proxy/__init__.py b/lms/djangoapps/rss_proxy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/rss_proxy/admin.py b/lms/djangoapps/rss_proxy/admin.py new file mode 100644 index 0000000000..fa5af3753b --- /dev/null +++ b/lms/djangoapps/rss_proxy/admin.py @@ -0,0 +1,7 @@ +""" +Admin module for the rss_proxy djangoapp. +""" +from django.contrib import admin +from rss_proxy.models import WhitelistedRssUrl + +admin.site.register(WhitelistedRssUrl) diff --git a/lms/djangoapps/rss_proxy/migrations/0001_initial.py b/lms/djangoapps/rss_proxy/migrations/0001_initial.py new file mode 100644 index 0000000000..973b94bd63 --- /dev/null +++ b/lms/djangoapps/rss_proxy/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='WhitelistedRssUrl', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('url', models.CharField(unique=True, max_length=255, db_index=True)), + ], + ), + ] diff --git a/lms/djangoapps/rss_proxy/migrations/__init__.py b/lms/djangoapps/rss_proxy/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/rss_proxy/models.py b/lms/djangoapps/rss_proxy/models.py new file mode 100644 index 0000000000..5694b0096c --- /dev/null +++ b/lms/djangoapps/rss_proxy/models.py @@ -0,0 +1,20 @@ +""" +Models for the rss_proxy djangoapp. +""" +from django.db import models +from model_utils.models import TimeStampedModel + + +class WhitelistedRssUrl(TimeStampedModel): + """ + Model for persisting RSS feed URLs which are whitelisted + for proxying via this rss_proxy djangoapp. + """ + url = models.CharField(max_length=255, unique=True, db_index=True) + + class Meta(object): + """ Meta class for this Django model """ + app_label = "rss_proxy" + + def __unicode__(self): + return unicode(self.url) diff --git a/lms/djangoapps/rss_proxy/tests/__init__.py b/lms/djangoapps/rss_proxy/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/rss_proxy/tests/test_models.py b/lms/djangoapps/rss_proxy/tests/test_models.py new file mode 100644 index 0000000000..c880eee623 --- /dev/null +++ b/lms/djangoapps/rss_proxy/tests/test_models.py @@ -0,0 +1,19 @@ +""" +Tests for the rss_proxy models +""" +from django.test import TestCase +from rss_proxy.models import WhitelistedRssUrl + + +class WhitelistedRssUrlTests(TestCase): + """ Tests for the rss_proxy.WhitelistedRssUrl model """ + + def setUp(self): + super(WhitelistedRssUrlTests, self).setUp() + self.whitelisted_rss_url = WhitelistedRssUrl.objects.create(url='http://www.example.com') + + def test_unicode(self): + """ + Test the unicode function returns the url + """ + self.assertEqual(unicode(self.whitelisted_rss_url), self.whitelisted_rss_url.url) diff --git a/lms/djangoapps/rss_proxy/tests/test_views.py b/lms/djangoapps/rss_proxy/tests/test_views.py new file mode 100644 index 0000000000..90a499e19a --- /dev/null +++ b/lms/djangoapps/rss_proxy/tests/test_views.py @@ -0,0 +1,69 @@ +""" +Tests for the rss_proxy views +""" +from django.test import TestCase +from django.core.urlresolvers import reverse +from mock import patch, Mock +from rss_proxy.models import WhitelistedRssUrl + + +class RssProxyViewTests(TestCase): + """ Tests for the rss_proxy views """ + + def setUp(self): + super(RssProxyViewTests, self).setUp() + + self.whitelisted_url1 = 'http://www.example.com' + self.whitelisted_url2 = 'http://www.example.org' + self.non_whitelisted_url = 'http://www.example.net' + self.rss = ''' + + + + + http://www.example.com/rss + + en + + Example + http://www.example.com/rss/item + Example item description + Fri, 13 May 1977 00:00:00 +0000 + + + + ''' + WhitelistedRssUrl.objects.create(url=self.whitelisted_url1) + WhitelistedRssUrl.objects.create(url=self.whitelisted_url2) + + @patch('rss_proxy.views.requests.get') + def test_proxy_with_whitelisted_url(self, mock_requests_get): + """ + Test the proxy view with a whitelisted URL + """ + mock_requests_get.return_value = Mock(status_code=200, content=self.rss) + resp = self.client.get('%s?url=%s' % (reverse('rss_proxy:proxy'), self.whitelisted_url1)) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp['Content-Type'], 'application/xml') + self.assertEqual(resp.content, self.rss) + + @patch('rss_proxy.views.requests.get') + def test_proxy_with_whitelisted_url_404(self, mock_requests_get): + """ + Test the proxy view with a whitelisted URL that is not found + """ + mock_requests_get.return_value = Mock(status_code=404) + resp = self.client.get('%s?url=%s' % (reverse('rss_proxy:proxy'), self.whitelisted_url2)) + print resp.status_code + print resp.content + print resp['Content-Type'] + self.assertEqual(resp.status_code, 404) + self.assertEqual(resp['Content-Type'], 'application/xml') + self.assertEqual(resp.content, '') + + def test_proxy_with_non_whitelisted_url(self): + """ + Test the proxy view with a non-whitelisted URL + """ + resp = self.client.get('%s?url=%s' % (reverse('rss_proxy:proxy'), self.non_whitelisted_url)) + self.assertEqual(resp.status_code, 404) diff --git a/lms/djangoapps/rss_proxy/urls.py b/lms/djangoapps/rss_proxy/urls.py new file mode 100644 index 0000000000..b61772c9e3 --- /dev/null +++ b/lms/djangoapps/rss_proxy/urls.py @@ -0,0 +1,9 @@ +""" +URLs for the rss_proxy djangoapp. +""" +from django.conf.urls import url + + +urlpatterns = [ + url(r"^$", "rss_proxy.views.proxy", name="proxy"), +] diff --git a/lms/djangoapps/rss_proxy/views.py b/lms/djangoapps/rss_proxy/views.py new file mode 100644 index 0000000000..023d0529a6 --- /dev/null +++ b/lms/djangoapps/rss_proxy/views.py @@ -0,0 +1,38 @@ +""" +Views for the rss_proxy djangoapp. +""" +import requests + +from django.conf import settings +from django.core.cache import cache +from django.http import HttpResponse, HttpResponseNotFound +from rss_proxy.models import WhitelistedRssUrl + + +CACHE_KEY_RSS = "rss_proxy.{url}" + + +def proxy(request): + """ + Proxy requests for the given RSS url if it has been whitelisted. + """ + + url = request.GET.get('url') + if url and WhitelistedRssUrl.objects.filter(url=url).exists(): + # Check cache for RSS if the given url is whitelisted + cache_key = CACHE_KEY_RSS.format(url=url) + status_code = 200 + rss = cache.get(cache_key, '') + print cache_key + print 'Cached rss: %s' % rss + if not rss: + # Go get the RSS from the URL if it was not cached + resp = requests.get(url) + status_code = resp.status_code + if status_code == 200: + # Cache RSS + rss = resp.content + cache.set(cache_key, rss, settings.RSS_PROXY_CACHE_TIMEOUT) + return HttpResponse(rss, status=status_code, content_type='application/xml') + + return HttpResponseNotFound() diff --git a/lms/envs/common.py b/lms/envs/common.py index 301fb0816c..8c8ae502c6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1860,6 +1860,9 @@ INSTALLED_APPS = ( # Microsite configuration 'microsite_configuration', + # RSS Proxy + 'rss_proxy', + # Student Identity Reverification 'reverification', @@ -2656,6 +2659,9 @@ MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.Filebas # TTL for microsite database template cache MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60 +################################ Settings for rss_proxy ################################ + +RSS_PROXY_CACHE_TIMEOUT = 3600 # The length of time we cache RSS retrieved from remote URLs in seconds #### PROCTORING CONFIGURATION DEFAULTS diff --git a/lms/urls.py b/lms/urls.py index 747fad08bb..085d50664e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -101,6 +101,7 @@ urlpatterns = ( url(r'^api/commerce/', include('commerce.api.urls', namespace='commerce_api')), url(r'^api/credit/', include('openedx.core.djangoapps.credit.urls', app_name="credit", namespace='credit')), + url(r'^rss_proxy/', include('rss_proxy.urls', namespace='rss_proxy')), ) if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]: