From 0a2adeaddbeb7d4cd14cdd710007c6843e33a2d6 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Wed, 13 Jun 2018 09:44:24 -0400 Subject: [PATCH] Add new 'resume' module for getting back to a workflow that was interrupted by a necessary redirect --- get_together/settings.py | 2 + get_together/tests/__init__.py | 1 + get_together/tests/speakers.py | 32 ++++++++++++++++ get_together/views/speakers.py | 5 ++- resume/__init__.py | 1 + resume/api.py | 29 +++++++++++++++ resume/apps.py | 5 +++ resume/middleware.py | 67 ++++++++++++++++++++++++++++++++++ resume/tests.py | 33 +++++++++++++++++ 9 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 get_together/tests/speakers.py create mode 100644 resume/__init__.py create mode 100644 resume/api.py create mode 100644 resume/apps.py create mode 100644 resume/middleware.py create mode 100644 resume/tests.py diff --git a/get_together/settings.py b/get_together/settings.py index 0e61c6a..e294101 100644 --- a/get_together/settings.py +++ b/get_together/settings.py @@ -54,6 +54,7 @@ INSTALLED_APPS = [ 'get_together', 'events', 'accounts', + 'resume', ] LOGIN_URL = 'login' @@ -82,6 +83,7 @@ MIDDLEWARE = [ 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 'social_django.middleware.SocialAuthExceptionMiddleware', + 'resume.middleware.ResumeMiddleware', ] ROOT_URLCONF = 'get_together.urls' diff --git a/get_together/tests/__init__.py b/get_together/tests/__init__.py index 58bf730..8ddcb50 100644 --- a/get_together/tests/__init__.py +++ b/get_together/tests/__init__.py @@ -2,3 +2,4 @@ from django.test import TestCase from .events import * from .event_reminder import * +from .speakers import * diff --git a/get_together/tests/speakers.py b/get_together/tests/speakers.py new file mode 100644 index 0000000..72122fe --- /dev/null +++ b/get_together/tests/speakers.py @@ -0,0 +1,32 @@ +from django.test import TestCase, Client +from django.shortcuts import resolve_url +from model_mommy import mommy + +from django.contrib.auth.models import User +from events.models import Speaker, Talk + +# Create your tests here. +class TalkCreationTests(TestCase): + + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + + def test_resume_adding_talk(self): + user = User.objects.create(username='testuser', password='12345', is_active=True) + + c = Client() + response = c.force_login(user) + + response = c.get(resolve_url('add-talk')) + assert(response.status_code == 302) + assert(response.url == resolve_url('add-speaker')) + + response = c.get(resolve_url('add-speaker')) + assert(response.status_code == 200) + response = c.post(resolve_url('add-speaker'), {'title': 'test', 'bio': 'testing'}) + assert(response.status_code == 302) + assert(response.url == resolve_url('add-talk')) + diff --git a/get_together/views/speakers.py b/get_together/views/speakers.py index 96f7d3d..105e119 100644 --- a/get_together/views/speakers.py +++ b/get_together/views/speakers.py @@ -23,6 +23,8 @@ from events.models.speakers import Speaker, Talk, Presentation, SpeakerRequest import datetime import simplejson +from resume import set_resume, resume_or_redirect + from .teams import * from .events import * @@ -63,7 +65,7 @@ def add_speaker(request): speaker_form = SpeakerBioForm(request.POST, request.FILES, instance=new_speaker) if speaker_form.is_valid(): new_speaker = speaker_form.save() - return redirect('user-talks') + return resume_or_redirect(request, 'user-talks') else: context = { 'speaker': new_speaker, @@ -138,6 +140,7 @@ def show_talk(request, talk_id): def add_talk(request): if Speaker.objects.filter(user=request.user.profile).count() < 1: messages.add_message(request, messages.WARNING, message=_('You must create a new Speaker profile before you can add a talk')) + set_resume(request) return redirect('add-speaker') new_talk = Talk() diff --git a/resume/__init__.py b/resume/__init__.py new file mode 100644 index 0000000..f30677e --- /dev/null +++ b/resume/__init__.py @@ -0,0 +1 @@ +from resume.api import * diff --git a/resume/api.py b/resume/api.py new file mode 100644 index 0000000..c858b40 --- /dev/null +++ b/resume/api.py @@ -0,0 +1,29 @@ +from django.shortcuts import redirect, resolve_url + +class ResumeFailure(Exception): + pass + + +def set_resume(request): + try: + resume_points = request._resume_points + except AttributeError: + if not hasattr(request, 'META'): + raise TypeError( + "add_resume_point() argument must be an HttpRequest object, not " + "'%s'." % request.__class__.__name__ + ) + raise ResumeFailure( + 'You cannot add resume points without installing ' + 'resume.middleware.ResumeMiddleware' + ) + else: + return resume_points.add(request.get_full_path()) + + +def resume_or_redirect(request, to, permanent=False, *args, **kwargs): + if len(request._resume_points) > 0: + resume_from = request._resume_points.pop() + return redirect(resume_from) + else: + return redirect(to, permanent, *args, **kwargs) diff --git a/resume/apps.py b/resume/apps.py new file mode 100644 index 0000000..5a5ad72 --- /dev/null +++ b/resume/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ResumeConfig(AppConfig): + name = 'Resume' diff --git a/resume/middleware.py b/resume/middleware.py new file mode 100644 index 0000000..3f15617 --- /dev/null +++ b/resume/middleware.py @@ -0,0 +1,67 @@ +import json +from django.utils.deprecation import MiddlewareMixin + + +class ResumeStorage: + session_key = '_resume' + + def __init__(self, request): + self.request = request + self._resume_points = self.load() + + def __len__(self): + return len(self._resume_points) + + def __iter__(self): + return iter(self._resume_points) + + def __contains__(self, item): + return item in self._resume_points + + def load(self): + """ + Retrieve a list of resume points from the request's session. + """ + if self.session_key not in self.request.session: + return [] + return json.loads(self.request.session.get(self.session_key)) + + def store(self): + """ + Store a list of resume points to the request's session. + """ + if self._resume_points: + self.request.session[self.session_key] = json.dumps(self._resume_points) + else: + self.request.session.pop(self.session_key, None) + return [] + + def add(self, path): + self._resume_points.append(path) + + def pop(self): + if len(self._resume_points) > 0: + return self._resume_points.pop() + else: + return None + + +class ResumeMiddleware(MiddlewareMixin): + """ + Middleware that handles setting resume points in a user flow. + """ + + def process_request(self, request): + request._resume_points = ResumeStorage(request) + + def process_response(self, request, response): + """ + Update the storage backend (i.e., save the resume points). + + Raise ValueError if not all resume points could be stored and DEBUG is True. + """ + # A higher middleware layer may return a request which does not contain + # resume storage, so make no assumption that it will be there. + if hasattr(request, '_resume_points'): + request._resume_points.store() + return response \ No newline at end of file diff --git a/resume/tests.py b/resume/tests.py new file mode 100644 index 0000000..d8239d1 --- /dev/null +++ b/resume/tests.py @@ -0,0 +1,33 @@ +from django.test import TestCase +from django.http.request import HttpRequest +from resume import set_resume +from resume.middleware import ResumeStorage + +# Create your tests here. +class ResumeTests(TestCase): + + def setUp(self): + super().setUp() + self.request = HttpRequest() + self.request.path = 'test/foo' + self.request.session = {} + self.request._resume_points = ResumeStorage(self.request) + + def tearDown(self): + super().tearDown() + del self.request + + def test_resume_point_storage(self): + + assert(len(self.request._resume_points) == 0) + + no_resume_point = self.request._resume_points.pop() + assert(no_resume_point is None) + + set_resume(self.request) + + assert(len(self.request._resume_points) == 1) + + one_resume_point = self.request._resume_points.pop() + assert(one_resume_point == 'test/foo') + assert(len(self.request._resume_points) == 0)