Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add unit tests. All of them.

- Add unit tests for the main functionality of the site. Trivial
  or features better suited to end-to-end testing were skipped.
- Fix a small issue where users could leave feedback on task attempts
  that did not belong to them.
  • Loading branch information...
commit 5c0d18d78f4fee457066d774668398212dbcd5a7 1 parent 41519ca
@Osmose Osmose authored
View
1  manage.py
@@ -7,6 +7,7 @@
# Edit this if necessary or override the variable in your environment.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oneanddone.settings')
+os.environ.setdefault('CELERY_LOADER', 'django')
# Add a temporary path so that we can import the funfactory
tmp_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
View
3  oneanddone/base/helpers.py
@@ -1,3 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from django.conf import settings
from jingo import register
View
10 oneanddone/base/tests/__init__.py
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from test_utils import TestCase as BaseTestCase
+
+
+class TestCase(BaseTestCase):
+ """Base class for tests across the site."""
+ def shortDescription(self):
+ return None
View
35 oneanddone/base/tests/test_helpers.py
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.test.utils import override_settings
+
+from mock import patch
+from nose.tools import eq_
+
+from oneanddone.base.helpers import less_css
+from oneanddone.base.tests import TestCase
+
+
+class LessCssTests(TestCase):
+ @override_settings(TEMPLATE_DEBUG=False)
+ def test_template_debug_false_call_css(self):
+ """If TEMPLATE_DEBUG is false, call jingo_minify.helpers.css."""
+ with patch('oneanddone.base.helpers.css') as mock_css:
+ eq_(less_css('bundle'), mock_css.return_value)
+ mock_css.assert_called_with('bundle')
+
+ @override_settings(TEMPLATE_DEBUG=True)
+ def test_template_debug_true_less_tags(self):
+ """
+ If TEMPLATE_DEBUG is true, return a set of link tags with
+ stylesheet/less as the rel attribute.
+ """
+ with patch('oneanddone.base.helpers.get_css_urls') as get_css_urls:
+ get_css_urls.return_value = ['foo', 'bar']
+ output = less_css('bundle')
+ get_css_urls.assert_called_with('bundle')
+
+ self.assertHTMLEqual(output, """
+ <link rel="stylesheet/less" media="screen,projection,tv" href="foo" />
+ <link rel="stylesheet/less" media="screen,projection,tv" href="bar" />
+ """)
View
31 oneanddone/base/tests/test_util.py
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.base.util import get_object_or_none
+
+
+class GetObjectOrNoneTests(TestCase):
+ def test_get(self):
+ """
+ If no exceptions are raised, return the value returned by
+ Model.objects.get.
+ """
+ Model = Mock()
+ eq_(get_object_or_none(Model, foo='bar'), Model.objects.get.return_value)
+ Model.objects.get.assert_called_with(foo='bar')
+
+ def test_none(self):
+ """
+ If a DoesNotExist or MultipleObjectsReturned exception is
+ raised, return None.
+ """
+ Model = Mock(DoesNotExist=Exception, MultipleObjectsReturned=Exception)
+ Model.objects.get.side_effect = Model.DoesNotExist
+ eq_(get_object_or_none(Model, foo='bar'), None)
+
+ Model.objects.get.side_effect = Model.MultipleObjectsReturned
+ eq_(get_object_or_none(Model, foo='bar'), None)
View
35 oneanddone/base/tests/test_views.py
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock, patch
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.base.views import HomeView
+
+
+class HomeViewTests(TestCase):
+ def setUp(self):
+ self.view = HomeView()
+ self.view.request = Mock()
+
+ def test_get_authenticated_redirect(self):
+ """
+ If the current user is authenticated, redirect them to the
+ profile detail view.
+ """
+ self.view.request.user.is_authenticated.return_value = True
+
+ with patch('oneanddone.base.views.redirect') as redirect:
+ eq_(self.view.get(), redirect.return_value)
+ redirect.assert_called_with('users.profile.detail')
+
+ def test_get_unauthenticated_super(self):
+ """
+ If the current user isn't authenticated, call the parent method.
+ """
+ self.view.request.user.is_authenticated.return_value = False
+
+ with patch('oneanddone.base.views.HomeView.get') as parent_get:
+ eq_(self.view.get('baz', foo='bar'), parent_get.return_value)
+ parent_get.assert_called_with('baz', foo='bar')
View
38 oneanddone/tasks/tests/__init__.py
@@ -0,0 +1,38 @@
+from factory import DjangoModelFactory, fuzzy, Sequence, SubFactory
+
+from oneanddone.tasks import models
+from oneanddone.users.tests import UserFactory
+
+
+class TaskAreaFactory(DjangoModelFactory):
+ FACTORY_FOR = models.TaskArea
+
+ name = Sequence(lambda n: 'test{0}'.format(n))
+ creator = SubFactory(UserFactory)
+
+
+class TaskFactory(DjangoModelFactory):
+ FACTORY_FOR = models.Task
+
+ area = SubFactory(TaskAreaFactory)
+ name = Sequence(lambda n: 'test{0}'.format(n))
+ short_description = Sequence(lambda n: 'test_description{0}'.format(n))
+ instructions = Sequence(lambda n: 'test_instructions{0}'.format(n))
+ execution_time = fuzzy.FuzzyInteger(0, 60)
+ is_draft = False
+ creator = SubFactory(UserFactory)
+
+
+class TaskAttemptFactory(DjangoModelFactory):
+ FACTORY_FOR = models.TaskAttempt
+
+ user = SubFactory(UserFactory)
+ task = SubFactory(TaskFactory)
+ state = models.TaskAttempt.STARTED
+
+
+class FeedbackFactory(DjangoModelFactory):
+ FACTORY_FOR = models.Feedback
+
+ attempt = SubFactory(TaskAttemptFactory)
+ text = Sequence(lambda n: 'feedback{0}'.format(n))
View
44 oneanddone/tasks/tests/test_admin.py
@@ -0,0 +1,44 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.admin import RecordCreatorMixin
+
+
+class FakeModelAdmin(object):
+ def save_model(self, request, obj, form, change):
+ return 'saved'
+
+
+class FakeModelAdminWithMixin(RecordCreatorMixin, FakeModelAdmin):
+ pass
+
+
+class RecordCreatorMixinTests(TestCase):
+ def setUp(self):
+ self.model_admin = FakeModelAdminWithMixin()
+
+ def test_save_model_no_pk(self):
+ """
+ If an object isn't saved yet (has no pk), set the creator to the
+ request's current user.
+ """
+ obj = Mock(pk=None)
+ request = Mock(user='foo')
+
+ self.model_admin.save_model(request, obj, None, False)
+ eq_(obj.creator, request.user)
+
+ def test_save_model_with_pk(self):
+ """
+ If an object exists in the DB (has a pk), do not change the
+ creator.
+ """
+ obj = Mock(pk=5, creator='bar')
+ request = Mock(user='foo')
+
+ self.model_admin.save_model(request, obj, None, False)
+ eq_(obj.creator, 'bar')
View
68 oneanddone/tasks/tests/test_filters.py
@@ -0,0 +1,68 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.filters import AvailableTasksFilterSet, TreeFilter
+from oneanddone.tasks.models import TaskArea
+from oneanddone.tasks.tests import TaskAreaFactory, TaskFactory
+
+
+class TreeFilterTests(TestCase):
+ def test_filter(self):
+ """
+ Filter should match objects that are related to the given value
+ or it's descendants via the field specified in the filter name.
+ """
+ # root -> child1 -> grandchild1 -> great_grandchild1
+ # \-> child2
+ root = TaskAreaFactory.create()
+ child1, child2 = TaskAreaFactory.create_batch(2, parent=root)
+ grandchild1 = TaskAreaFactory.create(parent=child1)
+ great_grandchild1 = TaskAreaFactory.create(parent=grandchild1)
+
+ # Should match all areas "below" child1.
+ tree_filter = TreeFilter(name='parent')
+ areas = tree_filter.filter(TaskArea.objects.all(), child1)
+ eq_(set(areas), set([grandchild1, great_grandchild1]))
+
+
+class AvailableTasksFilterSetTests(TestCase):
+ def test_area_filter_only_with_available_tasks(self):
+ """
+ Only TaskAreas with available tasks and their parents should be
+ included in the area filter.
+ """
+ # root -> child1 -> grandchild1
+ # \-> child2
+ root = TaskAreaFactory.create()
+ child1, child2 = TaskAreaFactory.create_batch(2, parent=root)
+ grandchild1 = TaskAreaFactory.create(parent=child1)
+
+ # Only grandchild1 has available tasks.
+ TaskFactory.create(area=grandchild1, is_draft=False)
+
+ # Area should include grandlchild1 and its parents.
+ filter_set = AvailableTasksFilterSet()
+ areas = filter_set.filters['area'].extra['queryset']
+ eq_(set(areas), set([root, child1, grandchild1]))
+
+ def test_area_filter_empty_children(self):
+ """
+ If a TaskArea has available tasks but its children don't, the
+ children should not be included in the area filter.
+ """
+ # root -> child1 -> grandchild1
+ # \-> child2
+ root = TaskAreaFactory.create()
+ child1, child2 = TaskAreaFactory.create_batch(2, parent=root)
+ grandchild1 = TaskAreaFactory.create(parent=child1)
+
+ # Only child1 has available tasks.
+ TaskFactory.create(area=child1, is_draft=False)
+
+ # Area should include child1, but not grandchild1.
+ filter_set = AvailableTasksFilterSet()
+ areas = filter_set.filters['area'].extra['queryset']
+ eq_(set(areas), set([root, child1]))
View
33 oneanddone/tasks/tests/test_helpers.py
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import urlparse
+
+from mock import Mock
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.helpers import page_url
+
+
+class PageUrlTests(TestCase):
+ def test_basic(self):
+ """
+ page_url should return a relative link to the current page,
+ preserving the GET arguments from the given request, and adding
+ a page parameter for the given page.
+ """
+ request = Mock(GET={'foo': 'bar', 'baz': 5})
+ url = urlparse.urlsplit(page_url(request, 4))
+ args = urlparse.parse_qs(url.query)
+ eq_(args, {'foo': ['bar'], 'baz': ['5'], 'page': ['4']})
+
+ def test_existing_page_arg(self):
+ """
+ If the current page already has a page GET argument, override
+ it.
+ """
+ request = Mock(GET={'foo': 'bar', 'page': 5})
+ url = urlparse.urlsplit(page_url(request, 4))
+ args = urlparse.parse_qs(url.query)
+ eq_(args, {'foo': ['bar'], 'page': ['4']})
View
37 oneanddone/tasks/tests/test_mixins.py
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock, patch
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks import mixins
+
+
+class TaskMustBePublishedMixinTests(TestCase):
+ def make_view(self, queryset, allow_expired_tasks_attr):
+ """
+ Create a fake view that applies the mixin to the given queryset
+ when get_queryset is called.
+ """
+ class BaseView(object):
+ def get_queryset(self):
+ return queryset
+
+ class View(mixins.TaskMustBePublishedMixin, BaseView):
+ allow_expired_tasks = allow_expired_tasks_attr
+
+ return View()
+
+ def test_get_queryset(self):
+ """
+ get_queryset should filter the parent class' queryset with the
+ availability filter from Task.
+ """
+ queryset = Mock()
+ view = self.make_view(queryset, False)
+
+ with patch('oneanddone.tasks.mixins.Task') as Task:
+ eq_(view.get_queryset(), queryset.filter.return_value)
+ queryset.filter.assert_called_with(Task.is_available_filter.return_value)
+ Task.is_available_filter.assert_called_with(allow_expired=False)
View
147 oneanddone/tasks/tests/test_models.py
@@ -0,0 +1,147 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from datetime import datetime
+
+from django.utils import timezone
+
+from mock import patch
+from nose.tools import eq_, ok_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.models import Task
+from oneanddone.tasks.tests import TaskAreaFactory, TaskFactory
+
+
+def aware_datetime(*args, **kwargs):
+ return timezone.make_aware(datetime(*args, **kwargs), timezone.utc)
+
+
+class TaskAreaTests(TestCase):
+ def test_full_name(self):
+ root = TaskAreaFactory.create(name='root')
+ child1 = TaskAreaFactory.create(parent=root, name='child1')
+ child2 = TaskAreaFactory.create(parent=child1, name='child2')
+
+ eq_(child2.full_name, 'root > child1 > child2')
+
+
+class TaskTests(TestCase):
+ def setUp(self):
+ self.task_draft = TaskFactory.create(is_draft=True)
+ self.task_no_draft = TaskFactory.create(is_draft=False)
+ self.task_start_jan = TaskFactory.create(
+ is_draft=False, start_date=aware_datetime(2014, 1, 1))
+ self.task_end_jan = TaskFactory.create(is_draft=False, end_date=aware_datetime(2014, 1, 1))
+ self.task_range_jan_feb = TaskFactory.create(
+ is_draft=False, start_date=aware_datetime(2014, 1, 1),
+ end_date=aware_datetime(2014, 2, 1))
+
+ def test_isnt_available_draft(self):
+ """
+ If a task is marked as a draft, it should not be available.
+ """
+ eq_(self.task_draft.is_available, False)
+
+ def test_isnt_available_before_start_date(self):
+ """
+ If the current datetime is before the start date of the task, it
+ should not be available.
+ """
+ with patch('oneanddone.tasks.models.timezone.now') as now:
+ now.return_value = aware_datetime(2013, 12, 1)
+ eq_(self.task_start_jan.is_available, False)
+
+ def test_is_available_after_start_date(self):
+ """
+ If the current datetime is after the start date of the task, it
+ should be available.
+ """
+ with patch('oneanddone.tasks.models.timezone.now') as now:
+ now.return_value = aware_datetime(2014, 2, 1)
+ eq_(self.task_start_jan.is_available, True)
+
+ def test_isnt_available_after_end_date(self):
+ """
+ If the current datetime is after the end date of the task, it
+ should not be available.
+ """
+ with patch('oneanddone.tasks.models.timezone.now') as now:
+ now.return_value = aware_datetime(2014, 2, 1)
+ eq_(self.task_end_jan.is_available, False)
+
+ def test_is_available_before_end_date(self):
+ """
+ If the current datetime is before the end date of the task, it
+ should be available.
+ """
+ with patch('oneanddone.tasks.models.timezone.now') as now:
+ now.return_value = aware_datetime(2013, 12, 1)
+ eq_(self.task_end_jan.is_available, True)
+
+ def test_is_available_within_dates(self):
+ """
+ If the current datetime is within the start and end date of the
+ task, it should be available.
+ """
+ with patch('oneanddone.tasks.models.timezone.now') as now:
+ now.return_value = aware_datetime(2014, 1, 5)
+ eq_(self.task_range_jan_feb.is_available, True)
+
+ def test_is_available_filter_default_now(self):
+ """
+ If no timezone is given, is_available_filter should use
+ timezone.now to determine the current datetime.
+ """
+ with patch('oneanddone.tasks.models.timezone.now') as now:
+ now.return_value = aware_datetime(2014, 1, 5)
+ tasks = Task.objects.filter(Task.is_available_filter())
+ expected = [self.task_no_draft, self.task_start_jan, self.task_range_jan_feb]
+ eq_(set(tasks), set(expected))
+
+ def test_is_available_filter_draft(self):
+ """
+ If a task is marked as a draft, it should not be available.
+ """
+ tasks = Task.objects.filter(Task.is_available_filter(now=aware_datetime(2014, 1, 2)))
+ ok_(self.task_no_draft in tasks)
+ ok_(self.task_draft not in tasks)
+
+ def test_is_available_filter_before_start_date(self):
+ """
+ If it is before a task's start date, the task should not be
+ available.
+ """
+ tasks = Task.objects.filter(Task.is_available_filter(now=aware_datetime(2013, 12, 1)))
+ ok_(self.task_start_jan not in tasks)
+
+ def test_is_available_filter_after_start_date(self):
+ """
+ If it is after a task's start date, the task should be
+ available.
+ """
+ tasks = Task.objects.filter(Task.is_available_filter(now=aware_datetime(2014, 2, 1)))
+ ok_(self.task_start_jan in tasks)
+
+ def test_is_available_filter_before_end_date(self):
+ """
+ If it is before a task's end date, the task should be available.
+ """
+ tasks = Task.objects.filter(Task.is_available_filter(now=aware_datetime(2013, 12, 1)))
+ ok_(self.task_end_jan in tasks)
+
+ def test_is_available_filter_after_end_date(self):
+ """
+ If it is after a task's end date, the task should not be
+ available.
+ """
+ tasks = Task.objects.filter(Task.is_available_filter(now=aware_datetime(2014, 2, 1)))
+ ok_(self.task_end_jan not in tasks)
+
+ def test_is_available_filter_in_range(self):
+ """
+ If the current date is within a task's date range, the task
+ should be available.
+ """
+ tasks = Task.objects.filter(Task.is_available_filter(now=aware_datetime(2014, 1, 5)))
+ ok_(self.task_range_jan_feb in tasks)
View
123 oneanddone/tasks/tests/test_views.py
@@ -0,0 +1,123 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.http import Http404
+
+from mock import Mock, patch
+from nose.tools import eq_, ok_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks import views
+from oneanddone.tasks.models import TaskAttempt
+from oneanddone.tasks.tests import TaskAttemptFactory, TaskFactory
+from oneanddone.users.tests import UserFactory
+
+
+class TaskDetailViewTests(TestCase):
+ def setUp(self):
+ self.view = views.TaskDetailView()
+
+ def test_get_context_data_not_authenticated(self):
+ """
+ If the current user isn't authenticated, don't include an
+ attempt in the context.
+ """
+ self.view.request = Mock()
+ self.view.request.user.is_authenticated.return_value = False
+
+ with patch('oneanddone.tasks.views.generic.DetailView.get_context_data') as get_context_data:
+ get_context_data.return_value = {}
+ ctx = self.view.get_context_data()
+ ok_('attempt' not in ctx)
+
+ def test_get_context_data_authenticated(self):
+ """
+ If the current user is authenticated, fetch their attempt for
+ the current task using get_object_or_none.
+ """
+ self.view.request = Mock()
+ self.view.request.user.is_authenticated.return_value = True
+ self.view.object = Mock()
+
+ get_object_patch = patch('oneanddone.tasks.views.get_object_or_none')
+ context_patch = patch('oneanddone.tasks.views.generic.DetailView.get_context_data')
+ with get_object_patch as get_object_or_none, context_patch as get_context_data:
+ get_context_data.return_value = {}
+ ctx = self.view.get_context_data()
+
+ eq_(ctx['attempt'], get_object_or_none.return_value)
+ get_object_or_none.assert_called_with(TaskAttempt, user=self.view.request.user,
+ task=self.view.object, state=TaskAttempt.STARTED)
+
+
+class StartTaskViewTests(TestCase):
+ def setUp(self):
+ self.view = views.StartTaskView()
+ self.task = TaskFactory.create()
+ self.view.get_object = Mock(return_value=self.task)
+
+ def test_post_existing_attempts(self):
+ """
+ If the user has an existing task attempt, redirect them to the
+ profile detail page.
+ """
+ attempt = TaskAttemptFactory.create()
+ self.view.request = Mock(user=attempt.user)
+
+ with patch('oneanddone.tasks.views.redirect') as redirect:
+ eq_(self.view.post(), redirect.return_value)
+ redirect.assert_called_with('users.profile.detail')
+ ok_(not TaskAttempt.objects.filter(user=attempt.user, task=self.task).exists())
+
+ def test_post_unavailable_task(self):
+ """
+ If the task is unavailable, redirect to the available tasks view
+ without creating an attempt.
+ """
+ self.task.is_draft = True
+ self.task.save()
+ user = UserFactory.create()
+ self.view.request = Mock(user=user)
+
+ with patch('oneanddone.tasks.views.redirect') as redirect:
+ eq_(self.view.post(), redirect.return_value)
+ redirect.assert_called_with('tasks.available')
+ ok_(not TaskAttempt.objects.filter(user=user, task=self.task).exists())
+
+ def test_post_create_attempt(self):
+ """
+ If the task is available and the user doesn't have any tasks in
+ progress, create a new task attempt and redirect to its page.
+ """
+ user = UserFactory.create()
+ self.view.request = Mock(user=user)
+
+ with patch('oneanddone.tasks.views.redirect') as redirect:
+ eq_(self.view.post(), redirect.return_value)
+ redirect.assert_called_with(self.task)
+ ok_(TaskAttempt.objects.filter(user=user, task=self.task, state=TaskAttempt.STARTED)
+ .exists())
+
+
+class CreateFeedbackViewTests(TestCase):
+ def setUp(self):
+ self.view = views.CreateFeedbackView()
+
+ def test_missing_attempt_404(self):
+ """
+ If there is no task attempt with the given ID, return a 404.
+ """
+ request = Mock(user=UserFactory.create())
+ with self.assertRaises(Http404):
+ self.view.dispatch(request, pk=9999)
+
+ def test_feedback_not_your_attempt(self):
+ """
+ If the current user doesn't match the user for the requested
+ task attempt, return a 404.
+ """
+ attempt = TaskAttemptFactory.create()
+ request = Mock(user=UserFactory.create())
+
+ with self.assertRaises(Http404):
+ self.view.dispatch(request, pk=attempt.pk)
View
5 oneanddone/tasks/views.py
@@ -6,7 +6,7 @@
from django.views import generic
from django_filters.views import FilterView
-from rest_framework import generics, permissions
+from rest_framework import generics
from tower import ugettext as _
from oneanddone.base.util import get_object_or_none
@@ -45,7 +45,6 @@ class StartTaskView(UserProfileRequiredMixin, TaskMustBePublishedMixin,
model = Task
def post(self, *args, **kwargs):
-
# Do not allow users to take more than one task at a time
if self.request.user.attempts_in_progress.exists():
messages.error(self.request, _('You may only work on one task at a time.'))
@@ -90,7 +89,7 @@ class CreateFeedbackView(UserProfileRequiredMixin, TaskMustBePublishedMixin, gen
template_name = 'tasks/feedback.html'
def dispatch(self, request, *args, **kwargs):
- self.attempt = get_object_or_404(TaskAttempt, pk=kwargs['pk'],
+ self.attempt = get_object_or_404(TaskAttempt, pk=kwargs['pk'], user=request.user,
state__in=[TaskAttempt.FINISHED, TaskAttempt.ABANDONED])
return super(CreateFeedbackView, self).dispatch(request, *args, **kwargs)
View
19 oneanddone/users/tests/__init__.py
@@ -0,0 +1,19 @@
+from django.contrib.auth.models import User
+
+from factory import DjangoModelFactory, Sequence, SubFactory
+
+from oneanddone.users import models
+
+
+class UserFactory(DjangoModelFactory):
+ FACTORY_FOR = User
+
+ username = Sequence(lambda n: 'test{0}'.format(n))
+ email = Sequence(lambda n: 'test{0}@example.com'.format(n))
+
+
+class UserProfileFactory(DjangoModelFactory):
+ FACTORY_FOR = models.UserProfile
+
+ user = SubFactory(UserFactory)
+ name = Sequence(lambda n: 'test{0}'.format(n))
View
45 oneanddone/users/tests/test_mixins.py
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock, patch
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.users.mixins import BaseUserProfileRequiredMixin
+from oneanddone.users.tests import UserFactory, UserProfileFactory
+
+
+class FakeMixin(object):
+ def dispatch(self, request, *args, **kwargs):
+ return 'fakemixin'
+
+
+class FakeView(BaseUserProfileRequiredMixin, FakeMixin):
+ pass
+
+
+class UserProfileRequiredMixinTests(TestCase):
+ def setUp(self):
+ self.view = FakeView()
+
+ def test_no_profile(self):
+ """
+ If the user hasn't created a profile, redirect them to the
+ profile creation view.
+ """
+ request = Mock()
+ request.user = UserFactory.create()
+
+ with patch('oneanddone.users.mixins.redirect') as redirect:
+ eq_(self.view.dispatch(request), redirect.return_value)
+ redirect.assert_called_with('users.profile.create')
+
+ def test_has_profile(self):
+ """
+ If the user has created a profile, call the parent class's
+ dispatch method.
+ """
+ request = Mock()
+ request.user = UserProfileFactory.create().user
+
+ eq_(self.view.dispatch(request), 'fakemixin')
View
52 oneanddone/users/tests/test_models.py
@@ -0,0 +1,52 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.models import TaskAttempt
+from oneanddone.tasks.tests import TaskAttemptFactory
+from oneanddone.users.tests import UserFactory, UserProfileFactory
+
+
+class UserTests(TestCase):
+ def test_unicode(self):
+ """
+ The string representation of a user should include their
+ email address.
+ """
+ user = UserProfileFactory.create(name='Foo Bar', user__email='foo@example.com').user
+ eq_(unicode(user), u'Foo Bar <foo@example.com>')
+
+ def test_unicode_no_name(self):
+ """
+ If a user has no display name, use "Anonymous" in its place.
+ """
+ user = UserFactory.build(email='foo@example.com')
+ eq_(unicode(user), u'Anonymous <foo@example.com>')
+
+ def test_display_name(self):
+ """
+ The display_name attribute should use the name from the user's
+ profile.
+ """
+ user = UserProfileFactory.create(name='Foo Bar').user
+ eq_(user.display_name, 'Foo Bar')
+
+ def test_display_name_no_profile(self):
+ """
+ If the user has no profile, user.display_name should be None.
+ """
+ user = UserFactory.build()
+ eq_(user.display_name, None)
+
+ def test_attempts_finished_count(self):
+ user = UserFactory.create()
+ TaskAttemptFactory.create_batch(4, user=user, state=TaskAttempt.FINISHED)
+ TaskAttemptFactory.create(user=user, state=TaskAttempt.STARTED)
+ eq_(user.attempts_finished_count, 4)
+
+ def test_attempts_in_progress(self):
+ user = UserFactory.create()
+ tasks = TaskAttemptFactory.create_batch(4, user=user, state=TaskAttempt.STARTED)
+ eq_(set(user.attempts_in_progress), set(tasks))
View
35 oneanddone/users/tests/test_views.py
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock, patch
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.users import views
+from oneanddone.users.tests import UserFactory, UserProfileFactory
+
+
+class CreateProfileViewTests(TestCase):
+ def setUp(self):
+ self.view = views.CreateProfileView()
+
+ def test_dispatch_existing_profile(self):
+ """
+ If the user already has a profile, redirect them to the profile
+ detail page.
+ """
+ request = Mock()
+ request.user = UserProfileFactory.create().user
+
+ with patch('oneanddone.users.views.redirect') as redirect:
+ eq_(self.view.dispatch(request), redirect.return_value)
+ redirect.assert_called_with('users.profile.detail')
+
+ def test_dispatch_no_profile(self):
+ """If the user has no profile, dispatch the request normally."""
+ request = Mock()
+ request.user = UserFactory.create()
+
+ with patch('oneanddone.users.views.generic.CreateView.dispatch') as dispatch:
+ eq_(self.view.dispatch(request), dispatch.return_value)
+ dispatch.assert_called_with(request)
View
2  requirements/dev.txt
@@ -4,3 +4,5 @@
-r ../vendor/src/funfactory/funfactory/requirements/compiled.txt
-r ../vendor/src/funfactory/funfactory/requirements/dev.txt
+
+factory-boy==2.3.0

0 comments on commit 5c0d18d

Please sign in to comment.
Something went wrong with that request. Please try again.