def test_harvard_research_access_request(auth_client, mailoutbox): user = auth_client.auth_user # viewing form with non-harvard email doesn't work response = auth_client.get(reverse('harvard-research-request-intro'), follow=True) check_response(response, content_includes="You are not currently using a harvard.edu email address") response = auth_client.get(reverse('harvard-research-request'), follow=True) check_response(response, content_includes="You are not currently using a harvard.edu email address") # viewing as harvard email does work user.email = '*****@*****.**' user.save() response = auth_client.get(reverse('harvard-research-request')) check_response(response, content_includes="Sign Agreement") # can submit form values = { 'name': 'First Last', 'title': 'Title', 'area_of_interest': 'Area of Interest' } response = auth_client.post(reverse('harvard-research-request'), values) check_response(response, status_code=302) assert response.url == reverse('harvard-research-request-success') # check created contract and email message research_request = user.harvard_contracts.first() message = mailoutbox[0].body for k, v in values.items(): assert getattr(research_request, k) == v assert v in message user.refresh_from_db() assert user.harvard_access is True
def test_change_api_key(auth_user, auth_client, client, mailoutbox): # Check/store original API key as visible to user original_token = auth_user.get_api_key() response = auth_client.get(reverse('user-details')) check_response(response) content = re.sub(r'\s+', ' ', response.content.decode()).strip() assert original_token in content # Make sure warning/confirmation template is rendered on get request response = auth_client.get(reverse('reset-api-key')) check_response(response) content = re.sub(r'\s+', ' ', response.content.decode()).strip() assert "This change takes place immediately and cannot be undone." in content # Change API key through web request response = auth_client.post(reverse('reset-api-key')) assert response.status_code == 302 assert response.url.endswith('/user/details') # Make sure new API key is in place and immediately visible to user auth_user.refresh_from_db() response = auth_client.get(reverse('user-details')) check_response(response) content = re.sub(r'\s+', ' ', response.content.decode()).strip() assert auth_user.get_api_key() in content assert original_token not in content # Make sure mail is sent and contains the correct new and old API keys message = mailoutbox[0].body assert "Your Case.law API key reset is complete" in message # Make sure auth is in place unauth_response = client.post(reverse('reset-api-key')) assert unauth_response.status_code == 302 assert "/user/login/" in unauth_response.url
def test_fetch(client, auth_client, elasticsearch, case_factory): cases = [ case_factory(jurisdiction__whitelisted=True, first_page_order=1, last_page_order=2), case_factory(jurisdiction__whitelisted=False, first_page_order=1, last_page_order=2), case_factory(jurisdiction__whitelisted=False, first_page_order=1, last_page_order=2), ] cites = [c.citations.first() for c in cases] text = f""" {cites[0].cite} {"A"*50} {cites[1].cite} {"A"*50} 123 {cites[2].cite} 123 """ response = client.post(reverse('fetch'), {'q': text}) check_response(response, content_includes=[ cites[0].cite, cases[0].full_cite(), f'... {"A"*39}', cites[1].cite, cases[1].full_cite(), f'{"A"*29} ...', ' 123', cites[2].cite, cases[2].full_cite(), ' 123\n', ]) # can't download zip when logged out check_response(client.post(reverse('fetch'), {'download': '1', 'case_ids': [c.pk for c in cases]}), status_code=403) # can download zip of valid PDFs response = auth_client.post(reverse('fetch'), {'download': '1', 'case_ids': [c.pk for c in cases]}) zip = ZipFile(BytesIO(b''.join(response))) for case in cases: path = 'cases/' + case.get_pdf_name() doc = fitz.open(stream=zip.open(path).read(), filetype='pdf') assert len(list(doc.pages())) == 2 # quota is tracked auth_client.auth_user.refresh_from_db() assert auth_client.auth_user.case_allowance_remaining == 498
def test_contact(client, auth_client, mailoutbox): # email field is empty if logged out response = client.get(reverse('contact')) soup = BeautifulSoup(response.content.decode(), 'html.parser') assert not soup.find('input', {'id': 'id_email'}).get('value') # email field is filled if logged in response = auth_client.get(reverse('contact')) soup = BeautifulSoup(response.content.decode(), 'html.parser') assert soup.find('input', { 'id': 'id_email' }).get('value') == auth_client.auth_user.email # submitting form will send an email post_vals = { 'subject': 'subject', 'box2': 'body', 'email': '*****@*****.**' } response = client.post(reverse('contact'), post_vals) check_response(response, status_code=302) assert len(mailoutbox) == 1 # submitting box1 will not send an email post_vals = { 'subject': 'subject', 'box1': 'body', 'box2': 'body', 'email': '*****@*****.**' } response = client.post(reverse('contact'), post_vals) check_response(response, status_code=302) # form pretends to succeed assert len(mailoutbox) == 1
def test_timeline_update(client, auth_client): tl = Timeline.objects.create(created_by=auth_client.auth_user, timeline=timeline) response = auth_client.get(retrieve_url + str(tl.id)) check_response(response, content_type="application/json") assert response.json()["timeline"]["title"] == timeline["title"] new_title = "My second timeline attempt" timeline["title"] = new_title update_url = reverse('labs:chronolawgic-api-update', args=[str(tl.id)]) response = auth_client.post(update_url, {"timeline": timeline}, format='json') check_response(response, content_type="application/json") assert response.json()["timeline"]["title"] == new_title new_title = "My third timeline attempt" timeline["title"] = new_title update_url = reverse('labs:chronolawgic-api-update', args=[str(tl.id)]) # don't allow unauthenticated users response = client.post(update_url, {"timeline": timeline}, format='json') check_response(response, status_code=403, content_type="application/json") response = auth_client.get(retrieve_url + str(tl.id)) assert response.json()["timeline"]["title"] != timeline["title"]
def test_retrieve_page_image(admin_client, auth_client, volume_metadata): volume_metadata.pdf_file = "fake_volume.pdf" volume_metadata.save() response = admin_client.get(reverse('page_image', args=[volume_metadata.pk, '2'], host='cite')) check_response(response, content_type="image/png") assert b'\x89PNG' in response.content response = auth_client.get(reverse('page_image', args=[volume_metadata.pk, '2'], host='cite')) check_response(response, status_code=302)
def test_redirect_following_login(auth_user, auth_client): """if ?next=url is not set, user gets directed to '/' after login""" auth_client.logout() response = auth_client.get(reverse('login')) check_response(response) password = '******' assert auth_user.check_password(password) response = auth_client.post(reverse('login'), { 'username': auth_user.email, 'password': password}) check_response(response, status_code=302) assert response.url == '/'
def test_registration_after_login(auth_user, auth_client): response = auth_client.get(reverse('user-details')) check_response(response) # try going to the registration page # get directed to the details page instead response = auth_client.get(reverse('register')) check_response(response, status_code=302) assert response.url == reverse('user-details') # make sure registration is still reachable after logging out auth_client.logout() response = auth_client.get(reverse('register')) check_response(response, status_code=200) assert "<title>Register | Caselaw Access Project</title>" in response.content.decode()
def test_cache_headers_with_bad_auth(client, case): # visiting homepage when logged out is cached ... response = client.get(reverse('home')) assert is_cached(response) # ... but visiting with a bad Authorization header is not cached client.credentials(HTTP_AUTHORIZATION='Token fake') response = client.get(reverse('home')) assert not is_cached(response) # ... and visiting with a bad session cookie is not cached client.credentials() client.cookies = SimpleCookie({settings.SESSION_COOKIE_NAME: 'fake'}) response = client.get(reverse('home')) assert not is_cached(response)
def test_login_wrong_password(auth_user, client): response = client.post(reverse('login'), { 'username': auth_user.email, 'password': '******' }) check_response(response) assert "Please enter a correct email and password." in response.content.decode()
def test_delete_account(auth_user, auth_client): assert auth_user.deactivated_by_user is False response = auth_client.post(reverse('delete_account')) check_response(response, status_code=302) assert response.url == reverse('home') response = auth_client.post(reverse('login'), { 'username': auth_user.email, 'password': '******' }) check_response(response) assert "Please enter a correct email and password." in response.content.decode() auth_user.refresh_from_db() assert auth_user.deactivated_by_user is True assert auth_user.deactivated_date
def citation(request, series_slug, volume_number_slug, page_number, case_id=None, pdf=False, db_case=None): """ /<series_slug>/<volume_number>/<page_number>/ -- show requested case (or list of cases, or case not found page). /<series_slug>/<volume_number>/<page_number>/<case_id>/ -- show requested case, using case_id to find one of multiple cases at this cite """ # redirect if series slug or volume number slug is in the wrong format if not pdf and (slugify(series_slug) != series_slug or slugify(volume_number_slug) != volume_number_slug): return HttpResponseRedirect(reverse( 'citation', args=[slugify(series_slug), slugify(volume_number_slug), page_number] + ([case_id] if case_id else []), host='cite')) ### try to look up citation case = None resolved_case = None if case_id: try: case = CaseDocument.get(id=case_id) resolved_cases = ResolveDocument.search().query("match", source='cap').query("match", source_id=case_id).execute() if resolved_cases: resolved_case = resolved_cases[0] except NotFoundError: raise Http404
def contact(request): form = form_for_request(request, ContactForm) if request.method == 'POST' and form.is_valid(): data = form.data # Only send email if box2 is filled out and box1 is not. # box1 is display: none, so should never be filled out except by spam bots. if data.get('box2') and not data.get('box1'): send_contact_email(data.get('subject'), data.get('box2'), data.get('email')) logger.info("sent contact email: %s" % data) else: logger.info("suppressing invalid contact email: %s" % data) return HttpResponseRedirect(reverse('contact-success')) email_from = request.user.email if request.user.is_authenticated else "" form.initial = {"email": email_from} return render( request, 'contact.html', { "form": form, "email": settings.DEFAULT_FROM_EMAIL, 'page_image': 'img/og_image/contact.png', 'meta_description': 'Email us at %s or fill out this form. ' % settings.DEFAULT_FROM_EMAIL, })
def test_screenshot__parallel(client, live_server, settings, ngrammed_cases): # set up conditions for /screenshot/ route to work settings.SCREENSHOT_FEATURE = True settings.DEBUG = True # so view runs browser unsandboxed for docker live_server_port = live_server.url.rsplit(':', 1)[1] with mock.patch('capweb.views.safe_domains', ['case.test:%s' % live_server_port]): # url we want a screenshot of -- .graph-container in /trends/?q=the target_url = reverse('trends', port=live_server_port).replace( ':8000', '') + '?q=the' target_selector = '.graph-container' # check screenshot screenshot_url = page_image_url(target_url, targets=[target_selector], timeout=30) response = client.get(screenshot_url) check_response(response, content_type="image/png") # screenshot size doesn't seem to be consistent across host environments? # width, height = Image.open(BytesIO(response.content)).size # assert width == 664 # assert height == 400 # check fallback screenshot screenshot_url = page_image_url(target_url, targets=['.does_not_exist'], timeout=30) response = client.get(screenshot_url) check_response(response, content_type="image/jpeg") # check that we got the default fallback image, api.jpg width, height = Image.open(BytesIO(response.content)).size assert width == 1200 assert height == 630
def test_schema_in_case(client, restricted_case, unrestricted_case, elasticsearch): ### whitelisted case response = client.get(full_url(unrestricted_case)) check_response(response, content_includes=unrestricted_case.body_cache.html) schema = get_schema(response) assert schema["headline"] == unrestricted_case.name_abbreviation assert schema["author"]["name"] == unrestricted_case.court.name # if case is whitelisted, extra info about inaccessibility is not needed # https://developers.google.com/search/docs/data-types/paywalled-content assert "hasPart" not in schema ### blacklisted case response = client.post(reverse('set_cookie'), {'not_a_bot': 'yes', 'next': full_url(restricted_case)}, follow=True) check_response(response, content_includes=restricted_case.body_cache.html) schema = get_schema(response) assert schema["headline"] == restricted_case.name_abbreviation assert schema["author"]["name"] == restricted_case.court.name # if case is blacklisted, we include more data assert "hasPart" in schema assert schema["hasPart"]["isAccessibleForFree"] == 'False'
def verify_user(request, user_id, activation_nonce): """ Verify email and assign api token """ user = get_object_or_404(CapUser, pk=user_id) # This leaks a little info -- we reveal whether a user ID exists or not and whether it is verified or deactivated. # This seems acceptable to provide better messages to legitimate users. if user.email_verified: return render(request, 'registration/verified.html') if not user.is_active: return render( request, 'registration/verified.html', {'error': 'This account is not active and cannot be verified.'}) error = None mailing_list_message = "We have not signed you up for our newsletter, Lawvocado. Sign up any time from our homepage." try: user.authenticate_user(activation_nonce=activation_nonce) except PermissionDenied: error = mark_safe( "This verification code is invalid or expired. <a href='%s'>Resend verification</a>?" % reverse('resend-verification')) else: # user authenticated successfully # update API limits for first X users per day # users after this limit will have approved accounts, but we will have to go back manually to increase limits site_limits = SiteLimits.add_values(daily_signups=1) if site_limits.daily_signups < site_limits.daily_signup_limit: user.total_case_allowance = user.case_allowance_remaining = settings.API_CASE_DAILY_ALLOWANCE user.save() # sign them up for the mailing list if they selected the mailing_list checkbox. if settings.MAILCHIMP['api_key'] and user.mailing_list: try: mc_client = MailChimp(mc_api=settings.MAILCHIMP['api_key'], mc_user=settings.MAILCHIMP['api_user']) mc_client.lists.members.create( settings.MAILCHIMP['id'], { 'email_address': user.email, 'merge_fields': { 'LNAME': user.first_name, 'FNAME': user.last_name }, 'status': 'subscribed' }) mailing_list_message = "Also, thanks for signing up for our newsletter, Lawvocado." except MailChimpError as e: if e.args[0]['status'] == 400 and e.args[0][ 'title'] == 'Member Exists': mailing_list_message = "Also, thanks for your continued interest in our newsletter, " \ "Lawvocado. We'll keep you on our list." else: logger.exception( "Error adding user email %s to mailing list" % user.email) return render(request, 'registration/verified.html', { 'error': error, 'mailing_list_message': mailing_list_message, })
def request_unaffiliated_research_access(request): """ Submit request for unaffiliated research access """ name = "%s %s" % (request.user.first_name, request.user.last_name) form = form_for_request(request, UnaffiliatedResearchRequestForm, initial={ 'name': name, 'email': request.user.email }) if request.method == 'POST' and form.is_valid(): # save request object form.instance.user = request.user form.save() # send notice emails message = loader.get_template( 'research_request/emails/unaffiliated_request_email.txt').render({ 'data': form.cleaned_data, }) subject = 'CAP independent research scholar application for {}'.format( name) send_contact_email(subject, message, request.user.email) return HttpResponseRedirect( reverse('unaffiliated-research-request-success')) return render(request, 'research_request/unaffiliated_research_request.html', {'form': form})
def volume(request, series_slug, volume_number_slug): """ /<series_slug>/<volume_number>/ -- list all cases for given volumes (typically only one). """ # redirect if series slug or volume number slug is in the wrong format if slugify(series_slug) != series_slug or slugify(volume_number_slug) != volume_number_slug: return HttpResponseRedirect(reverse('volume', args=[slugify(series_slug), slugify(volume_number_slug)], host='cite')) vols = list(VolumeMetadata.objects .select_related('reporter') .filter(volume_number_slug=volume_number_slug, reporter__short_name_slug=series_slug, out_of_scope=False) .order_by('-second_part_of')) if not vols: raise Http404 cases_query = CaseDocument.search()\ .filter("term", volume__volume_number_slug=volume_number_slug)\ .filter("term", reporter__short_name_slug__raw=series_slug)\ .sort('first_page')\ .extra(size=10000)\ .source({"excludes": "casebody_data.*"}) cases = cases_query.execute() cases = natsorted(cases, key=lambda c: c.first_page) volumes = [(volume, [c for c in cases if c.volume.barcode == volume.barcode]) for volume in vols] return render(request, 'cite/volume.html', { "volumes": volumes, })
def test_view_user_details(auth_user, auth_client): """ User can see their API token """ response = auth_client.get(reverse('user-details')) check_response(response) content = re.sub(r'\s+', ' ', response.content.decode()).strip() assert auth_user.get_api_key() in content assert "Unlimited access:" not in content assert str(auth_user.total_case_allowance) in content # user can't see limit if they have unlimited access auth_user.unlimited_access = True auth_user.unlimited_access_until = timedelta(hours=24) + timezone.now() auth_user.save() response = auth_client.get(reverse('user-details')) check_response(response) content = re.sub(r'\s+', ' ', response.content.decode()).strip() assert "Unmetered access" in content
def delete_account(request): if request.method == 'POST': request.user.is_active = False request.user.deactivated_by_user = True request.user.save() return HttpResponseRedirect(reverse('home')) return render(request, 'registration/delete_account.html')
def docs_url(doc_link): """ Link to a documentation page. This can either be the value of the `doc_link:` key in the markdown file, if there is one, or else the text part of the filename ('example' from '01_example.md'). This allows us to consistently link to docs pages when they are moved or reordered. """ return reverse('docs', args=[doc_links[doc_link]])
def send_new_signup_email(request, user): token_url = reverse('verify-user', kwargs={'user_id':user.pk, 'activation_nonce': user.get_activation_nonce()}, scheme="https") send_mail( 'Caselaw Access Project: Verify your email address', "Please click here to verify your email address: \n\n%s \n\nIf you received this message in error, please ignore it." % token_url, settings.DEFAULT_FROM_EMAIL, [user.email], fail_silently=False, ) logger.info("sent new_signup email for %s" % user.email)
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # set label here because reverse() isn't ready when defining the class self.fields['mailing_list'].label = mark_safe( "<small>(optional)</small> Sign me up for the CAP newsletter: Lawvocado." ) self.fields['agreed_to_tos'].label = mark_safe( "I have read and agree to the <a href='%s' target='_blank'>Terms of Use</a>." % reverse('terms'))
def test_download_area(client, auth_client, unlimited_auth_client, tmp_path, monkeypatch): overlay_path = Path(settings.BASE_DIR, 'downloads') underlay_path = tmp_path monkeypatch.setattr("capweb.views.download_files_storage", DownloadOverlayStorage(location=str(underlay_path))) # listing should include files from BASE_DIR/downloads as well as from donload_files_storage location underlay_path.joinpath('underlay.txt').write_text("contents") response = client.get(reverse('download-files', args=[''])) check_response(response, content_type="application/json") response_json = response.json() assert set(f['name'] for f in response_json['files']) == (set(p.name for p in overlay_path.glob('*')) | {'underlay.txt'}) - {'README.md', '.DS_Store'} # can fetch contents from both overlay and underlay, with or without auth for c in (client, unlimited_auth_client): for read_from, path, content_type in ((overlay_path, 'README.md', 'text/markdown'), (underlay_path, 'underlay.txt', 'text/plain')): response = c.get(reverse('download-files', args=[path])) check_response(response, content_type=content_type, content_includes=read_from.joinpath(path).read_text()) assert is_cached(response) # can list overlay folders response = c.get(reverse('download-files', args=['scdb/'])) check_response(response, content_type="application/json") # restricted folder underlay_path.joinpath('restricted').mkdir() underlay_path.joinpath('restricted/file.txt').write_text('contents') for test_client, allow_downloads, token in ( (client, False, 'none'), (auth_client, False, auth_client.auth_user.get_api_key()), (unlimited_auth_client, True, unlimited_auth_client.auth_user.get_api_key()) ): token_client = CapClient() token_client.credentials(HTTP_AUTHORIZATION='Token ' + token) for c in (test_client, token_client): # restricted directory response = c.get(reverse('download-files', args=['restricted/'])) check_response(response, content_type="application/json") cacheable = c == client # we can only cache for an anonymous user who didn't supply an auth header assert is_cached(response) is cacheable response_json = response.json() assert response_json['allow_downloads'] == allow_downloads assert set(f['name'] for f in response_json['files']) == {'file.txt'} # restricted file response = c.get(reverse('download-files', args=['restricted/file.txt'])) assert not is_cached(response) if allow_downloads: check_response(response, content_type="text/plain", content_includes="contents") else: check_response(response, content_type="application/json", status_code=403) # symlinks underlay_path.joinpath('folder_link').symlink_to('restricted') underlay_path.joinpath('file_link.txt').symlink_to('restricted/file.txt') for p1, p2 in (('folder_link/', 'restricted/'), ('folder_link/file.txt', 'restricted/file.txt'), ('file_link.txt', 'restricted/file.txt')): response = c.get(reverse('download-files', args=[p1])) check_response(response, status_code=302) assert response.url == reverse('download-files', args=[p2])
def test_case_editor(reset_sequences, admin_client, auth_client, unrestricted_case_factory): unrestricted_case = unrestricted_case_factory(first_page_order=1, last_page_order=3) url = reverse('case_editor', args=[unrestricted_case.pk], host='cite') response = admin_client.get(url) check_response(response) response = auth_client.get(url) check_response(response, status_code=302) # make an edit unrestricted_case.sync_case_body_cache() body_cache = unrestricted_case.body_cache old_html = body_cache.html old_first_page = unrestricted_case.first_page description = "Made some edits" page = unrestricted_case.structure.pages.first() response = admin_client.post( url, json.dumps({ 'metadata': { 'name': [unrestricted_case.name, 'new name'], 'decision_date_original': [unrestricted_case.decision_date_original, '2020-01-01'], 'first_page': [old_first_page, 'ignore this'], 'human_corrected': [False, True], }, 'description': description, 'edit_list': { page.id: { 'BL_81.3': { 3: ["Case text 0", "Replacement text"], } } } }), content_type="application/json") check_response(response) # check OCR edit body_cache.refresh_from_db() new_html = body_cache.html assert list(unified_diff(old_html.splitlines(), new_html.splitlines(), n=0))[3:] == [ '- <h4 class="parties" id="b81-4">Case text 0</h4>', '+ <h4 class="parties" id="b81-4">Replacement text</h4>', ] # check metadata unrestricted_case.refresh_from_db() assert unrestricted_case.name == 'new name' assert unrestricted_case.decision_date_original == '2020-01-01' assert unrestricted_case.decision_date == datetime.date(year=2020, month=1, day=1) assert unrestricted_case.human_corrected is True assert unrestricted_case.first_page == old_first_page # change ignored # check log log_entry = unrestricted_case.correction_logs.first() assert log_entry.description == description assert log_entry.user_id == admin_client.auth_user.id
def confirm_login_allowed(self, user): """ Override AuthenticationForm to block login with unverified email address. """ if not user.email_verified: raise forms.ValidationError( mark_safe( "This email is registered but not yet verified. <a href='%s'>Resend verification</a>?" % reverse('resend-verification')), code='unverified', ) return super().confirm_login_allowed(user)
def test_cases_multiple(client, django_assert_num_queries, case_factory, elasticsearch): """ Test /series/volume/case/ with multiple matching cases """ three_cases = [case_factory( jurisdiction__whitelisted=True, citations__type='official', citations__cite='23 Ill. App. 19', citations__normalized_cite='23illapp19' ) for i in range(3)] first_case = three_cases[0] response = client.get(reverse('citation', args=['ill-app', '23', '19'], host='cite'), follow=True) check_response(response, content_includes='Multiple cases match') content = response.content.decode() for case in three_cases: assert case.name_abbreviation in content # load one of the results response = client.get(reverse('citation', args=['ill-app', '23', '19', first_case.id], host='cite')) check_response(response)
def test_resend_verification(client, mailoutbox): # create new user response = client.post(reverse('register'), { 'email': '*****@*****.**', 'first_name': 'First', 'last_name': 'Last', 'password1': 'Password2', 'password2': 'Password2', 'agreed_to_tos': 'on', }) check_response(response) assert len(mailoutbox) == 1 # resend verification response = client.post(reverse('resend-verification'), { 'email': '*****@*****.**', }) check_response(response) # same verification email sent assert mailoutbox[0].body == mailoutbox[1].body
def test_unaffiliated_research_access_request(auth_client, mailoutbox): # can view form response = auth_client.get(reverse('unaffiliated-research-request')) check_response(response) # can submit form values = { 'name': 'First Last', 'email': '*****@*****.**', 'area_of_interest': 'Foo Area of Interest' } response = auth_client.post(reverse('unaffiliated-research-request'), values) check_response(response, status_code=302) assert response.url == reverse('unaffiliated-research-request-success') # check created request and email message research_request = auth_client.auth_user.research_requests.first() message = mailoutbox[0].body for k, v in values.items(): assert getattr(research_request, k) == v assert v in message
def test_case_series_name_redirect(client, unrestricted_case, elasticsearch): """ Test /series/volume/case/ with series redirect when not slugified""" cite = unrestricted_case.citations.first() cite_parts = re.match(r'(\S+)\s+(.*?)\s+(\S+)$', cite.cite).groups() # series is not slugified, expect redirect response = client.get( reverse('citation', args=[cite_parts[1], cite_parts[0], cite_parts[2]], host='cite')) check_response(response, status_code=302) response = client.get( reverse('citation', args=[cite_parts[1], cite_parts[0], cite_parts[2]], host='cite'), follow=True) check_response(response) # series redirect works with case_id response = client.get( reverse('citation', args=[cite_parts[1], cite_parts[0], cite_parts[2], unrestricted_case.id], host='cite')) check_response(response, status_code=302) response = client.get( reverse('citation', args=[cite_parts[1], cite_parts[0], cite_parts[2]], host='cite'), follow=True) check_response(response)