From b7d9f4419cff3bb1805eb147793e70f2de637065 Mon Sep 17 00:00:00 2001 From: Benjamin Cutler Date: Fri, 3 Jan 2025 18:06:51 -0700 Subject: [PATCH] disable add/edit/delete on v1 (#748) [#188514696] --- tests/test_api.py | 563 ------------------------------------- tests/test_auth.py | 7 - tests/test_interstitial.py | 149 ---------- tests/test_prize.py | 7 - tests/test_speedrun.py | 10 - tests/test_viewutil.py | 123 -------- tracker/api_urls.py | 8 +- tracker/urls.py | 6 +- tracker/views/api.py | 383 +------------------------ 9 files changed, 12 insertions(+), 1244 deletions(-) delete mode 100644 tests/test_viewutil.py diff --git a/tests/test_api.py b/tests/test_api.py index 9c62a7a5e..27e7ab40a 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,14 +1,7 @@ import datetime -import json from decimal import Decimal -from unittest import skip -from django.contrib.admin.models import ADDITION as LogEntryADDITION -from django.contrib.admin.models import CHANGE as LogEntryCHANGE -from django.contrib.admin.models import DELETION as LogEntryDELETION -from django.contrib.admin.models import LogEntry from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType from django.core.serializers.json import DjangoJSONEncoder from django.test import override_settings from django.urls import reverse @@ -28,20 +21,6 @@ def format_time(dt): class TestGeneric(APITestCase): """generic cases that could apply to any class, even if they use a specific one for testing purposes""" - def test_add_with_bad_type(self): - request = self.factory.post('/api/v1/add', dict(type='nonsense')) - request.user = self.super_user - data = self.parseJSON(tracker.views.api.add(request), status_code=400) - self.assertEqual('Malformed Parameters', data['error']) - - def test_add_with_bad_field(self): - request = self.factory.post( - '/api/v1/add', dict(type='run', nonsense='nonsense') - ) - request.user = self.super_user - data = self.parseJSON(tracker.views.api.add(request), status_code=400) - self.assertEqual('Field does not exist', data['error']) - @override_settings(TRACKER_PAGINATION_LIMIT=20) def test_search_with_offset_and_limit(self): event = randgen.generate_event(self.rand, today_noon) @@ -87,108 +66,6 @@ def test_search_with_offset_and_limit(self): # bad request if offset is negative self.parseJSON(tracker.views.api.search(request), status_code=400) - def test_add_log(self): - request = self.factory.post( - '/api/v1/milestone', - dict(type='milestone', name='New Record', event=self.event.pk, amount=500), - ) - request.user = self.super_user - data = self.parseJSON(tracker.views.api.add(request)) - milestone = models.Milestone.objects.get(pk=data[0]['pk']) - add_entry = LogEntry.objects.order_by('-pk')[1] - self.assertEqual(int(add_entry.object_id), milestone.id) - self.assertEqual( - add_entry.content_type, ContentType.objects.get_for_model(models.Milestone) - ) - self.assertEqual(add_entry.action_flag, LogEntryADDITION) - change_entry = LogEntry.objects.order_by('-pk')[0] - self.assertEqual(int(change_entry.object_id), milestone.id) - self.assertEqual( - change_entry.content_type, - ContentType.objects.get_for_model(models.Milestone), - ) - self.assertEqual(change_entry.action_flag, LogEntryCHANGE) - self.assertIn('Set name to "%s".' % milestone.name, change_entry.change_message) - self.assertIn( - 'Set event to "%s".' % milestone.event.name, change_entry.change_message - ) - - def test_change_log(self): - old_milestone = models.Milestone.objects.create( - name='New Record', amount=500, event=self.event - ) - request = self.factory.post( - '/api/v1/edit', - dict( - type='milestone', - id=old_milestone.id, - name='New Record!', - start=250, - description='New event record!', - ), - ) - request.user = self.super_user - data = self.parseJSON(tracker.views.api.edit(request)) - milestone = models.Milestone.objects.get(pk=data[0]['pk']) - entry = LogEntry.objects.order_by('pk').last() - self.assertEqual(int(entry.object_id), milestone.id) - self.assertEqual( - entry.content_type, ContentType.objects.get_for_model(models.Milestone) - ) - self.assertEqual(entry.action_flag, LogEntryCHANGE) - self.assertIn( - 'Changed name from "%s" to "%s".' % (old_milestone.name, milestone.name), - entry.change_message, - ) - self.assertIn( - 'Changed description from empty to "%s".' % milestone.description, - entry.change_message, - ) - self.assertIn('Changed start from empty to "250"', entry.change_message) - - def test_change_log_m2m(self): - run = models.SpeedRun.objects.create(name='Test Run', run_time='0:15:00') - runner1 = models.Talent.objects.create(name='PJ') - runner2 = models.Talent.objects.create(name='trihex') - request = self.factory.post( - '/api/v1/edit', - dict(type='run', id=run.id, runners='%s,%s' % (runner1.name, runner2.name)), - ) - request.user = self.super_user - self.parseJSON(tracker.views.api.edit(request)) - entry = LogEntry.objects.order_by('pk').last() - self.assertEqual(int(entry.object_id), run.id) - self.assertEqual( - entry.content_type, ContentType.objects.get_for_model(models.SpeedRun) - ) - self.assertEqual(entry.action_flag, LogEntryCHANGE) - self.assertIn( - 'Changed runners from empty to "%s".' % ([str(runner1), str(runner2)],), - entry.change_message, - ) - - def test_delete_log(self): - milestone = models.Milestone.objects.create( - event=self.event, amount=500, name='New Record' - ) - request = self.factory.post( - '/api/v1/delete', dict(type='milestone', id=milestone.id) - ) - request.user = self.super_user - self.parseJSON(tracker.views.api.delete(request)) - self.assertFalse(models.Milestone.objects.filter(pk=milestone.pk).exists()) - entry = LogEntry.objects.order_by('pk').last() - self.assertEqual(int(entry.object_id), milestone.id) - self.assertEqual( - entry.content_type, ContentType.objects.get_for_model(models.Milestone) - ) - self.assertEqual(entry.action_flag, LogEntryDELETION) - - def test_blank_m2m(self): - request = self.factory.post('/api/vi/add', dict(type='run', runners='')) - request.user = self.super_user - self.parseJSON(tracker.views.api.add(request), status_code=400) - class TestSpeedRun(APITestCase): model_name = 'Speed Run' @@ -356,261 +233,6 @@ def test_get_endtime_gte(self): self.assertModelPresent(self.run4, data) self.assertModelPresent(self.run5, data) - def test_add_with_category(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Category', - run_time='0:15:00', - setup_time='0:05:00', - category='100%', - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - self.assertEqual(len(data), 1) - self.assertEqual(models.SpeedRun.objects.get(pk=data[0]['pk']).category, '100%') - - def test_edit_with_category(self): - request = self.factory.post( - '/api/v1/edit', dict(type='run', id=self.run2.id, category='100%') - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request)) - self.assertEqual(len(data), 1) - self.assertEqual(models.SpeedRun.objects.get(pk=data[0]['pk']).category, '100%') - - def test_add_with_runners_as_ids(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners='%d,%d' % (self.runner1.id, self.runner2.id), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_add_with_runners_as_invalid_ids(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners='%d,%d' % (self.runner1.id, 6666), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request), status_code=400) - self.assertEqual('Foreign Key relation could not be found', data['error']) - - def test_add_with_runners_as_json_ids(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners=json.dumps([self.runner1.id, self.runner2.id]), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_add_with_runners_as_names(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners='%s,%s' - % (self.runner1.name.upper(), self.runner2.name.lower()), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_add_with_runners_as_json_names(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners=json.dumps( - [self.runner1.name.upper(), self.runner2.name.lower()] - ), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_add_with_runners_as_json_natural_keys(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners=json.dumps( - [self.runner1.natural_key(), self.runner2.natural_key()] - ), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_add_with_runners_as_names_invalid(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='run', - name='Added Run With Runners', - runners='%s,%s' % (self.runner1.name, 'nonsense'), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request), status_code=400) - self.assertEqual('Foreign Key relation could not be found', data['error']) - - def test_edit_with_runners_as_ids(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', - id=self.run2.id, - runners='%d,%d' % (self.runner1.id, self.runner2.id), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_edit_with_runners_as_json_ids(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', - id=self.run2.id, - runners=json.dumps([self.runner1.id, self.runner2.id]), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_edit_with_runners_as_ids_invalid(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', id=self.run2.id, runners='%d,%d' % (self.runner1.id, 6666) - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request), status_code=400) - self.assertEqual('Foreign Key relation could not be found', data['error']) - - def test_edit_with_runners_as_names(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', - id=self.run2.id, - runners='%s,%s' - % (self.runner1.name.upper(), self.runner2.name.lower()), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_edit_with_runners_as_json_names(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', - id=self.run2.id, - runners=json.dumps( - [self.runner1.name.upper(), self.runner2.name.lower()] - ), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_edit_with_runners_as_json_natural_keys(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', - id=self.run2.id, - runners=json.dumps( - [self.runner1.natural_key(), self.runner2.natural_key()] - ), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request)) - self.assertEqual(len(data), 1) - self.assertSetEqual( - set(models.SpeedRun.objects.get(pk=data[0]['pk']).runners.all()), - {self.runner1, self.runner2}, - ) - - def test_edit_with_runners_as_names_invalid(self): - request = self.factory.post( - '/api/v1/edit', - dict( - type='run', - id=self.run2.id, - runners='%s,%s' % (self.runner1.name, 'nonsense'), - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.edit(request), status_code=400) - self.assertEqual('Foreign Key relation could not be found', data['error']) - def test_tech_notes_without_permission(self): request = self.factory.get( '/api/v1/search', dict(type='run', id=self.run1.id, tech_notes='') @@ -676,15 +298,6 @@ def test_name_case_insensitive_search(self): self.assertEqual(len(data), 1) self.assertEqual(data[0], expected) - @skip('readonly because of v2') - def test_name_case_insensitive_add(self): - request = self.factory.post( - '/api/v1/add', dict(type='runner', name=self.runner1.name.upper()) - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request), status_code=400) - self.assertRegex(data['messages'][0], 'case-insensitive.*already exists') - def test_search_by_event(self): request = self.factory.get( '/api/v1/search', dict(type='runner', event=self.event.id) @@ -855,46 +468,6 @@ def test_search_with_imagefile(self): self.assertEqual(len(data), 1) self.assertModelPresent(prize, data) - def test_add_with_new_category(self): - self.add_user.user_permissions.add( - Permission.objects.get(name='Can add Prize Category') - ) - request = self.factory.post( - '/api/v1/add', - dict( - type='prize', - name='Added Prize With Category', - event=json.dumps(self.event.natural_key()), - handler=json.dumps(self.add_user.natural_key()), - category='Grand', - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request)) - prize = models.Prize.objects.get(pk=data[0]['pk']) - self.assertEqual(len(data), 1) - # TODO: add and search don't format the same - # self.assertEqual(data[0], self.format_prize(prize)) - self.assertEqual( - prize.category, - models.PrizeCategory.objects.get(name='Grand'), - ) - - def test_add_with_new_category_without_category_add_permission(self): - request = self.factory.post( - '/api/v1/add', - dict( - type='prize', - name='Added Prize With Category', - event=json.dumps(self.event.natural_key()), - handler=json.dumps(self.add_user.natural_key()), - category='Grand', - ), - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.add(request), status_code=400) - self.assertEqual('Foreign Key relation could not be found', data['error']) - class TestEvent(APITestCase): model_name = 'event' @@ -1127,107 +700,6 @@ def test_bid_with_parent(self): self.assertModelPresent(self.format_bid(child, request), data) -@skip('currently disabled') -class TestDonor(APITestCase): - model_name = 'donor' - - def setUp(self): - super(TestDonor, self).setUp() - self.add_user.user_permissions.add( - Permission.objects.get(name='Can view full usernames') - ) - - @classmethod - def format_donor(cls, donor, request): - other_fields = {} - - if donor.visibility == 'FULL' or request.GET.get('donor_names', None) == '': - other_fields['firstname'] = donor.firstname - other_fields['lastname'] = donor.lastname - other_fields['alias'] = donor.alias - other_fields['alias_num'] = donor.alias_num - other_fields['canonical_url'] = request.build_absolute_uri( - donor.get_absolute_url() - ) - elif donor.visibility == 'FIRST': - other_fields['firstname'] = donor.firstname - other_fields['lastname'] = f'{donor.lastname[0]}...' - other_fields['alias'] = donor.alias - other_fields['alias_num'] = donor.alias_num - other_fields['canonical_url'] = request.build_absolute_uri( - donor.get_absolute_url() - ) - elif donor.visibility == 'ALIAS': - other_fields['alias'] = donor.alias - other_fields['alias_num'] = donor.alias_num - other_fields['canonical_url'] = request.build_absolute_uri( - donor.get_absolute_url() - ) - - return dict( - fields=dict( - public=donor.visible_name(), - visibility=donor.visibility, - **other_fields, - ), - model='tracker.donor', - pk=donor.id, - ) - - def test_full_visibility_donor(self): - donor = randgen.generate_donor(self.rand, visibility='FULL') - donor.save() - request = self.factory.get(reverse('tracker:api_v1:search'), dict(type='donor')) - request.user = self.anonymous_user - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 1) - self.assertEqual(data[0], self.format_donor(donor, request)) - - def test_first_name_visibility_donor(self): - donor = randgen.generate_donor(self.rand, visibility='FIRST') - donor.save() - request = self.factory.get(reverse('tracker:api_v1:search'), dict(type='donor')) - request.user = self.anonymous_user - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 1) - self.assertEqual(data[0], self.format_donor(donor, request)) - - def test_alias_visibility_donor(self): - donor = randgen.generate_donor(self.rand, visibility='ALIAS') - donor.save() - request = self.factory.get(reverse('tracker:api_v1:search'), dict(type='donor')) - request.user = self.anonymous_user - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 1) - self.assertEqual(data[0], self.format_donor(donor, request)) - - def test_anonymous_visibility_donor(self): - donor = randgen.generate_donor(self.rand, visibility='ANON') - donor.save() - request = self.factory.get(reverse('tracker:api_v1:search'), dict(type='donor')) - request.user = self.anonymous_user - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 0) - - def test_donor_full_names_without_permission(self): - request = self.factory.get( - reverse('tracker:api_v1:search'), dict(type='donor', donor_names='') - ) - request.user = self.anonymous_user - self.parseJSON(tracker.views.api.search(request), status_code=403) - - def test_donor_full_names_with_permission(self): - donor = randgen.generate_donor(self.rand, visibility='ALIAS') - donor.save() - request = self.factory.get( - reverse('tracker:api_v1:search'), dict(type='donor', donor_names='') - ) - request.user = self.add_user - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 1) - self.assertEqual(data[0], self.format_donor(donor, request)) - - class TestDonation(APITestCase): model_name = 'donation' @@ -1356,41 +828,6 @@ def test_donor_visibilities(self): msg=f'Visibility {visibility} gave an incorrect result', ) - @skip('disabled for now') - def test_search_by_donor(self): - donation = randgen.generate_donation( - self.rand, donor=self.donor, event=self.event - ) - donation.save() - - self.donor.alias = 'Foo' - self.donor.visibility = 'ALIAS' - self.donor.save() - - request = self.factory.get( - reverse('tracker:api_v1:search'), dict(type='donation', donor=self.donor.id) - ) - request.user = self.anonymous_user - - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 1) - self.assertModelPresent( - self.format_donation(donation, request), - data, - msg='Normal visibility gave an incorrect result', - ) - - self.donor.visibility = 'ANON' - self.donor.save() - - request = self.factory.get( - reverse('tracker:api_v1:search'), dict(type='donation', donor=self.donor.id) - ) - request.user = self.anonymous_user - - data = self.parseJSON(tracker.views.api.search(request)) - self.assertEqual(len(data), 0, msg='Anonymous donor was searchable') - class TestMilestone(APITestCase): model_name = 'milestone' diff --git a/tests/test_auth.py b/tests/test_auth.py index 16fe188fa..336988256 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,7 +1,5 @@ import urllib.parse -from unittest import skipIf -import django import post_office.models from django.contrib.auth import get_user_model from django.test import RequestFactory, TestCase, override_settings @@ -73,11 +71,6 @@ def test_register_inactive_user(self): ) self.assertContains(resp, 'An e-mail has been sent to your address.') - # TODO: remove skip when 3.2 no longer supported - @skipIf( - django.VERSION < (4, 1), - 'assertFormError requires response object until Django 4.1', - ) def test_register_active_user(self): AuthUser.objects.create( username='existinguser', email='test@email.com', is_active=True diff --git a/tests/test_interstitial.py b/tests/test_interstitial.py index aaa6e2761..4b612157e 100644 --- a/tests/test_interstitial.py +++ b/tests/test_interstitial.py @@ -112,155 +112,6 @@ def test_interstitials_for_run(self): } ) - def test_move_interstitial_up_within_run(self): - i1 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=1 - ) - i2 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=2 - ) - i3 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=3 - ) - self.client.force_login(self.superuser) - resp = self.client.post( - reverse('tracker:api_v1:interstitial'), - { - 'id': i3.id, - 'order': self.run2.order, - 'suborder': i2.suborder, - }, - ) - self.assertEqual(resp.status_code, 200) - self.assertInterstitialOrder({self.run2: [i1, i3, i2]}) - - def test_move_interstitial_down_within_run(self): - i1 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=1 - ) - i2 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=2 - ) - i3 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=3 - ) - self.client.force_login(self.superuser) - resp = self.client.post( - reverse('tracker:api_v1:interstitial'), - { - 'id': i1.id, - 'order': self.run2.order, - 'suborder': i2.suborder, - }, - ) - self.assertEqual(resp.status_code, 200) - self.assertInterstitialOrder({self.run2: [i2, i1, i3]}) - - def test_move_interstitial_up_between_run(self): - i1 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=1 - ) - i2 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=2 - ) - i3 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=3 - ) - i4 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=1 - ) - i5 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=2 - ) - i6 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=3 - ) - self.client.force_login(self.superuser) - resp = self.client.post( - reverse('tracker:api_v1:interstitial'), - { - 'id': i2.id, - 'order': self.run3.order, - 'suborder': i6.suborder, - }, - ) - self.assertEqual(resp.status_code, 200) - self.assertInterstitialOrder( - { - self.run2: [i1, i3], - self.run3: [i4, i5, i2, i6], - } - ) - - def test_move_interstitial_down_between_run(self): - i1 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=1 - ) - i2 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=2 - ) - i3 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=3 - ) - i4 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=1 - ) - i5 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=2 - ) - i6 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=3 - ) - self.client.force_login(self.superuser) - resp = self.client.post( - reverse('tracker:api_v1:interstitial'), - { - 'id': i5.id, - 'order': self.run2.order, - 'suborder': i2.suborder, - }, - ) - self.assertEqual(resp.status_code, 200) - self.assertInterstitialOrder( - { - self.run2: [i1, i5, i2, i3], - self.run3: [i4, i6], - } - ) - - def test_move_interstitial_fill_holes(self): - i1 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=1 - ) - i2 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=3 - ) - i3 = models.Interstitial.objects.create( - event=self.event1, order=self.run2.order, suborder=5 - ) - i4 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=3 - ) - i5 = models.Interstitial.objects.create( - event=self.event1, order=self.run3.order, suborder=7 - ) - self.client.force_login(self.superuser) - resp = self.client.post( - reverse('tracker:api_v1:interstitial'), - { - 'id': i2.id, - 'order': self.run3.order, - 'suborder': -1, - }, - ) - self.assertEqual(resp.status_code, 200) - self.assertInterstitialOrder( - { - self.run2: [i1, i3], - self.run3: [i4, i5, i2], - } - ) - # smoke test def test_full_schedule(self): ad = models.Ad.objects.create( diff --git a/tests/test_prize.py b/tests/test_prize.py index 29b7294de..3e2f44686 100644 --- a/tests/test_prize.py +++ b/tests/test_prize.py @@ -1,10 +1,8 @@ import datetime import random from decimal import Decimal -from unittest import skipIf from unittest.mock import patch -import django import post_office.models from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.models import User @@ -1463,11 +1461,6 @@ def test_prize_key_import_action(self): reverse('admin:tracker_prize_key_import', args=(self.prize_with_keys.id,)), ) - # TODO: remove skip when 3.2 no longer supported - @skipIf( - django.VERSION < (4, 1), - 'assertFormError requires response object until Django 4.1', - ) def test_prize_key_import_form(self): keys = ['dead-beef-dead-beef-123%d' % i for i in range(5)] response = self.client.get( diff --git a/tests/test_speedrun.py b/tests/test_speedrun.py index 3a1635da9..1cc3f5418 100755 --- a/tests/test_speedrun.py +++ b/tests/test_speedrun.py @@ -3,9 +3,7 @@ import random import zoneinfo from typing import Iterable, List, Optional, Union -from unittest import skipIf -import django from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.test import TransactionTestCase @@ -471,10 +469,6 @@ def test_start_run(self): self.assertEqual(self.run3.anchor_time, expected_start) self.assertEqual(self.run3.starttime, expected_start) - @skipIf( - django.VERSION < (4, 1), - 'assertFormError requires response object until Django 4.1', - ) def test_invalid_time(self): from tracker.admin.forms import StartRunForm @@ -491,10 +485,6 @@ def test_invalid_time(self): self.assertFalse(form.is_valid()) self.assertFormError(form, None, StartRunForm.Errors.invalid_start_time) - @skipIf( - django.VERSION < (4, 1), - 'assertFormError requires response object until Django 4.1', - ) def test_anchor_drift(self): from tracker.admin.forms import StartRunForm diff --git a/tests/test_viewutil.py b/tests/test_viewutil.py deleted file mode 100644 index 06cb566d7..000000000 --- a/tests/test_viewutil.py +++ /dev/null @@ -1,123 +0,0 @@ -import json - -from django.contrib.auth.models import User -from django.test import TransactionTestCase - -from tracker import models -from tracker.views import parse_value - -from .util import today_noon - - -class TestParseValue(TransactionTestCase): - def setUp(self): - super(TestParseValue, self).setUp() - self.event = models.Event.objects.create( - datetime=today_noon, targetamount=5, short='agdq2015' - ) - self.super_user = User.objects.create(username='superuser', is_superuser=True) - self.runner1 = models.Talent.objects.create(name='trihex') - self.runner2 = models.Talent.objects.create(name='PJ') - - def test_single_pk_fetch(self): - self.assertEqual( - parse_value(models.SpeedRun, 'event', self.event.pk), self.event - ) - - def test_single_pk_as_string_fetch(self): - self.assertEqual( - parse_value(models.SpeedRun, 'event', str(self.event.pk)), self.event - ) - - def test_single_natural_key_as_string_fetch(self): - self.assertEqual( - parse_value(models.SpeedRun, 'event', self.event.short), self.event - ) - - def test_single_natural_key_as_json_fetch(self): - self.assertEqual( - parse_value(models.SpeedRun, 'event', json.dumps(self.event.natural_key())), - self.event, - ) - - def test_single_natural_key_as_string_create(self): - runner = parse_value( - models.Submission, 'runner', 'UraniumAnchor', self.super_user - ) - self.assertTrue(runner.id) - self.assertEqual(runner.name, 'UraniumAnchor') - - def test_single_natural_key_as_json_create(self): - runner = parse_value( - models.Submission, 'runner', '["UraniumAnchor"]', self.super_user - ) - self.assertTrue(runner.id) - self.assertEqual(runner.name, 'UraniumAnchor') - - def test_complex_natural_key_as_json_create(self): - run = parse_value( - models.Bid, - 'speedrun', - '["Mega Man 3", ["%s"]]' % self.event.short, - self.super_user, - ) - self.assertTrue(run.id) - self.assertEqual(run.name, 'Mega Man 3') - self.assertEqual(run.event, self.event) - - def test_m2m_pk_csv_fetch(self): - expected_runners = [self.runner1, self.runner2] - runners = parse_value( - models.SpeedRun, 'runners', ','.join(str(r.pk) for r in expected_runners) - ) - self.assertEqual(runners, expected_runners) - - def test_m2m_pk_csv_bad_fetch(self): - with self.assertRaises(models.Talent.DoesNotExist): - parse_value(models.SpeedRun, 'runners', '1001,1002') - - def test_m2m_pk_json_fetch(self): - expected_runners = [self.runner1, self.runner2] - runners = parse_value( - models.SpeedRun, 'runners', json.dumps([r.pk for r in expected_runners]) - ) - self.assertEqual(runners, expected_runners) - - def test_m2m_pk_json_bad_fetch(self): - with self.assertRaises(models.Talent.DoesNotExist): - parse_value(models.SpeedRun, 'runners', '[1001,1002]') - - def test_m2m_natural_key_csv_fetch(self): - expected_runners = [self.runner1, self.runner2] - runners = parse_value( - models.SpeedRun, 'runners', ','.join(r.name for r in expected_runners) - ) - self.assertEqual(runners, expected_runners) - - def test_m2m_natural_key_csv_bad_fetch(self): - with self.assertRaises(models.Talent.DoesNotExist): - parse_value(models.SpeedRun, 'runners', 'total,nonsense') - - def test_m2m_natural_key_flat_json_fetch(self): - expected_runners = [self.runner1, self.runner2] - runners = parse_value( - models.SpeedRun, 'runners', json.dumps([r.name for r in expected_runners]) - ) - self.assertEqual(runners, expected_runners) - - def test_m2m_natural_key_flat_json_bad_fetch(self): - with self.assertRaises(models.Talent.DoesNotExist): - parse_value(models.SpeedRun, 'runners', '["total","nonsense"]') - - def test_m2m_natural_key_full_json_fetch(self): - expected_runners = [self.runner1, self.runner2] - runners = parse_value( - models.SpeedRun, - 'runners', - json.dumps([r.natural_key() for r in expected_runners]), - ) - self.assertEqual(runners, expected_runners) - - def test_m2m_natural_key_full_json_bad_fetch(self): - with self.assertRaises(models.Talent.DoesNotExist): - parse_value(models.SpeedRun, 'runners', '[["total"],["nonsense"]]') diff --git a/tracker/api_urls.py b/tracker/api_urls.py index 4a252402e..a85f4444b 100644 --- a/tracker/api_urls.py +++ b/tracker/api_urls.py @@ -6,14 +6,12 @@ urlpatterns = [ path('', api.root, name='root'), path('search/', api.search, name='search'), - path('add/', api.add, name='add'), - path('edit/', api.edit, name='edit'), - path('delete/', api.delete, name='delete'), + path('add/', api.gone, name='add'), + path('edit/', api.gone, name='edit'), + path('delete/', api.gone, name='delete'), path('command/', api.command, name='command'), path('me/', api.me, name='me'), # moved over from private repo, stopgap until v2 is ready path('ads//', api.ads, name='ads'), - path('interstitial/', api.interstitial_reorder, name='interstitial'), path('interviews//', api.interviews, name='interviews'), - path('hosts//', api.hosts), ] diff --git a/tracker/urls.py b/tracker/urls.py index 80ab2ec78..2a5699c67 100644 --- a/tracker/urls.py +++ b/tracker/urls.py @@ -47,9 +47,9 @@ path('ipn/', donateviews.ipn, name='ipn'), path('analytics/', analyticsviews.post_analytics, name='analytics'), path('search/', api.search), - path('add/', api.add), - path('edit/', api.edit), - path('delete/', api.delete), + path('add/', api.gone), + path('edit/', api.gone), + path('delete/', api.gone), path('command/', api.command), path('me/', api.me, name='me'), path('api/v1/', include(api_urls, namespace='api_v1')), diff --git a/tracker/views/api.py b/tracker/views/api.py index aab86dc01..30fd97642 100644 --- a/tracker/views/api.py +++ b/tracker/views/api.py @@ -2,7 +2,6 @@ from django.contrib import admin from django.contrib.auth.decorators import permission_required, user_passes_test -from django.contrib.auth.models import AnonymousUser from django.core import serializers from django.core.exceptions import ( FieldDoesNotExist, @@ -29,10 +28,10 @@ from django.db.utils import IntegrityError from django.http import Http404, HttpResponse, QueryDict from django.views.decorators.cache import cache_page, never_cache -from django.views.decorators.csrf import csrf_exempt, csrf_protect +from django.views.decorators.csrf import csrf_protect from django.views.decorators.http import require_GET, require_POST -from tracker import logutil, search_filters, settings +from tracker import search_filters, settings from tracker.models import ( Ad, Bid, @@ -40,7 +39,6 @@ Donation, DonationBid, Event, - Interstitial, Interview, Milestone, Prize, @@ -50,24 +48,18 @@ ) from tracker.search_filters import EventAggregateFilter, PrizeWinnersFilter from tracker.serializers import TrackerSerializer -from tracker.util import flatten from tracker.views import commands site = admin.site __all__ = [ 'search', - 'add', - 'edit', - 'delete', + 'gone', 'command', - 'parse_value', 'me', 'root', 'ads', 'interviews', - 'interstitial_reorder', - 'hosts', ] modelmap = { @@ -76,7 +68,6 @@ 'allbids': Bid, # TODO: remove this, special filters should not be top level types 'donationbid': DonationBid, 'donation': Donation, - # 'donor': Donor, 'headset': Talent, 'milestone': Milestone, 'event': Event, @@ -87,12 +78,6 @@ 'tag': Tag, } -# models end up here once they're added to v2, so that we can deprecate stuff piecemeal - -readonly_models = ('bid', 'bidtarget', 'allbids', 'runner', 'headset') - -permmap = {'run': 'speedrun'} - related = { 'run': ['priority_tag'], 'bid': ['speedrun', 'event', 'parent', 'parent__speedrun', 'parent__event'], @@ -159,7 +144,6 @@ }, 'donation': { '__self__': [ - # 'donor', 'event', 'domain', 'transactionstate', @@ -172,11 +156,7 @@ 'commentlanguage', 'pinned', ], - 'donor': ['alias', 'alias_num', 'visibility'], }, - # 'donor': { - # '__self__': ['alias', 'alias_num', 'firstname', 'lastname', 'visibility'], - # }, 'event': { '__self__': [ 'short', @@ -270,30 +250,10 @@ } -def donor_privacy_filter(fields): - visibility = fields['visibility'] - if visibility == 'FIRST' and fields['lastname']: - fields['lastname'] = fields['lastname'][0] + '...' - if visibility == 'ALIAS' or visibility == 'ANON': - del fields['lastname'] - del fields['firstname'] - if visibility == 'ANON': - del fields['alias'] - del fields['alias_num'] - del fields['canonical_url'] - - def donation_privacy_filter(fields): if fields['commentstate'] != 'APPROVED': del fields['comment'] del fields['commentlanguage'] - # FIXME?: these don't get filtered out on `all_comments` searches but maybe that's ok - # if fields['donor__visibility'] == 'ANON': - # del fields['donor'] - # del fields['donor__alias'] - # del fields['donor__alias_num'] - # del fields['donor__visibility'] - # del fields['donor__canonical_url'] def run_privacy_filter(fields): @@ -481,9 +441,7 @@ def search(request): ] = related_object.visible_name() else: obj['fields'][related_field + '__public'] = str(related_object) - if search_type == 'donor' and not donor_names: - donor_privacy_filter(obj['fields']) - elif search_type == 'donation' and not all_comments: + if search_type == 'donation' and not all_comments: donation_privacy_filter(obj['fields']) elif search_type == 'run' and not tech_notes: run_privacy_filter(obj['fields']) @@ -500,242 +458,13 @@ def search(request): return resp -def to_natural_key(key): - return key if isinstance(key, list) else [key] - - -def parse_value(Model, field, value, user=None): - user = user or AnonymousUser() - if value == 'None': - return None - else: - model_field = Model._meta.get_field(field) - RelatedModel = model_field.related_model - if RelatedModel is None: - return value - if model_field.many_to_many: - if value == '': - return [] - elif value[0] == '[': - try: - pks = json.loads(value) - except ValueError: - raise ValueError( - 'Value for field "%s" could not be parsed as json array for m2m lookup' - % (field,) - ) - else: - pks = value.split(',') - try: - results = list(RelatedModel.objects.filter(pk__in=pks)) - except (ValueError, TypeError): # could not parse pks - results = [ - RelatedModel.objects.get_by_natural_key(*to_natural_key(pk)) - for pk in pks - ] - if len(pks) != len(results): - bad_pks = set(pks) - set(m.pk for m in results) - raise RelatedModel.DoesNotExist('Invalid pks: %s' % (bad_pks)) - return results - else: - try: - return RelatedModel.objects.get(pk=value) - except ValueError: # if pk is not coercable - - def has_add_perm(): - return user.has_perm( - '%s.add_%s' - % (RelatedModel._meta.app_label, RelatedModel._meta.model_name) - ) - - try: - if value[0] in '"[{': - key = json.loads(value) - if not isinstance(key, list): - key = [key] - else: - key = [value] - except ValueError: - raise ValueError( - 'Value "%s" could not be parsed as json for natural key lookup on field "%s"' - % (value, field) - ) - if ( - hasattr(RelatedModel.objects, 'get_or_create_by_natural_key') - and has_add_perm() - ): - return RelatedModel.objects.get_or_create_by_natural_key(*key)[0] - else: - return RelatedModel.objects.get_by_natural_key(*key) - - def root(request): # only here to give a root access point raise Http404 -def get_admin(Model): - return admin.site._registry[Model] - - -def filter_fields(fields, model_admin, request, obj=None): - writable_fields = tuple(flatten(model_admin.get_fields(request, obj))) - readonly_fields = model_admin.get_readonly_fields(request, obj) - return [ - field - for field in fields - if (field in writable_fields and field not in readonly_fields) - ] - - -@csrf_exempt -@generic_api_view -@never_cache -@transaction.atomic -@require_POST -def add(request): - add_params = request.POST - add_type = add_params['type'] - Model = modelmap.get(add_type, None) - if Model is None: - raise KeyError('%s is not a recognized model type' % add_type) - if add_type in readonly_models: - raise PermissionDenied(f'{add_type} is not writeable via this api') - model_admin = get_admin(Model) - if not model_admin.has_add_permission(request): - raise PermissionDenied( - 'You do not have permission to add a model of the requested type' - ) - good_fields = filter_fields(add_params.keys(), model_admin, request) - bad_fields = set(good_fields) - set(add_params.keys()) - if bad_fields: - raise PermissionDenied( - 'You do not have permission to set the following field(s) on new objects: %s' - % ','.join(sorted(bad_fields)) - ) - newobj = Model() - changed_fields = [] - m2m_collections = [] - for k, v in add_params.items(): - if k in ('type', 'id'): - continue - new_value = parse_value(Model, k, v, request.user) - if isinstance(new_value, list): # accounts for m2m relationships - m2m_collections.append((k, new_value)) - new_value = [str(x) for x in new_value] - else: - setattr(newobj, k, new_value) - changed_fields.append('Set %s to "%s".' % (k, new_value)) - newobj.full_clean() - models = newobj.save() or [newobj] - for k, v in m2m_collections: - getattr(newobj, k).set(v) - logutil.addition(request, newobj) - logutil.change(request, newobj, ' '.join(changed_fields)) - resp = HttpResponse( - serializers.serialize('json', models, ensure_ascii=False), - content_type='application/json;charset=utf-8', - ) - if 'queries' in request.GET and request.user.has_perm('tracker.view_queries'): - return HttpResponse( - json.dumps(connection.queries, ensure_ascii=False, indent=1), - content_type='application/json;charset=utf-8', - ) - return resp - - -@csrf_exempt -@generic_api_view -@never_cache -@transaction.atomic -@require_POST -def delete(request): - delete_params = request.POST - delete_type = delete_params['type'] - Model = modelmap.get(delete_type, None) - if Model is None: - raise KeyError('%s is not a recognized model type' % delete_type) - if delete_type in readonly_models: - raise PermissionDenied(f'{delete_type} is not writeable via this api') - obj = Model.objects.get(pk=delete_params['id']) - model_admin = get_admin(Model) - if not model_admin.has_delete_permission(request, obj): - raise PermissionDenied('You do not have permission to delete that model') - logutil.deletion(request, obj) - obj.delete() - return HttpResponse( - json.dumps( - { - 'result': 'Object %s of type %s deleted' - % (delete_params['id'], delete_params['type']) - }, - ensure_ascii=False, - ), - content_type='application/json;charset=utf-8', - ) - - -@csrf_exempt -@generic_api_view -@never_cache -@transaction.atomic -@require_POST -def edit(request): - edit_params = request.POST - edit_type = edit_params['type'] - Model = modelmap.get(edit_type, None) - if Model is None: - raise KeyError('%s is not a recognized model type' % edit_type) - Model = modelmap[edit_type] - if edit_type in readonly_models: - raise PermissionDenied(f'{edit_type} is not writeable via this api') - model_admin = get_admin(Model) - obj = Model.objects.get(pk=edit_params['id']) - if not model_admin.has_change_permission(request, obj): - raise PermissionDenied('You do not have permission to change that object') - good_fields = filter_fields(edit_params.keys(), model_admin, request) - bad_fields = set(good_fields) - set(edit_params.keys()) - if bad_fields: - raise PermissionDenied( - 'You do not have permission to set the following field(s) on the requested object: %s' - % ','.join(sorted(bad_fields)) - ) - changed_fields = [] - for k, v in edit_params.items(): - if k in ('type', 'id'): - continue - old_value = getattr(obj, k) - if hasattr(old_value, 'all'): # accounts for m2m relationships - old_value = [str(x) for x in old_value.all()] - new_value = parse_value(Model, k, v, request.user) - if isinstance(new_value, list): # accounts for m2m relationships - getattr(obj, k).set(new_value) - new_value = [str(x) for x in new_value] - else: - setattr(obj, k, new_value) - if str(old_value) != str(new_value): - if old_value and not new_value: - changed_fields.append('Changed %s from "%s" to empty.' % (k, old_value)) - elif not old_value and new_value: - changed_fields.append('Changed %s from empty to "%s".' % (k, new_value)) - else: - changed_fields.append( - 'Changed %s from "%s" to "%s".' % (k, old_value, new_value) - ) - obj.full_clean() - models = obj.save() or [obj] - if changed_fields: - logutil.change(request, obj, ' '.join(changed_fields)) - resp = HttpResponse( - serializers.serialize('json', models, ensure_ascii=False), - content_type='application/json;charset=utf-8', - ) - if 'queries' in request.GET and request.user.has_perm('tracker.view_queries'): - return HttpResponse( - json.dumps(connection.queries, ensure_ascii=False, indent=1), - content_type='application/json;charset=utf-8', - ) - return resp +def gone(request): + return HttpResponse(status=410) @csrf_protect @@ -863,103 +592,3 @@ def interviews(request, event): content_type='application/json;charset=utf-8', ) return resp - - -@generic_api_view -@permission_required('tracker.change_interstitial', raise_exception=True) -@transaction.atomic -@require_POST -def interstitial_reorder(request): - try: - model = Interstitial.objects.get(id=request.POST['id']) - except Interstitial.DoesNotExist: - raise Http404 - if model.event.locked: - raise PermissionDenied - order = int(request.POST['order']) - suborder = int(request.POST['suborder']) - new_siblings = Interstitial.objects.filter(order=order) - last_sibling = new_siblings.last() - if suborder == -1: - if last_sibling: - suborder = last_sibling.suborder + 1 - else: - suborder = 1 - if suborder <= 0: - raise ValueError('suborder must be positive or -1') - if order != model.order: - old_siblings = Interstitial.objects.filter(order=model.order).exclude( - id=model.id - ) - slice1 = new_siblings.filter(suborder__lt=suborder).exclude(id=model.id) - slice2 = ( - new_siblings.filter(suborder__gte=suborder).exclude(id=model.id).reverse() - ) - else: - old_siblings = [] - if model.suborder > suborder: - slice1 = new_siblings.filter(suborder__lt=suborder).exclude(id=model.id) - slice2 = ( - new_siblings.filter(suborder__gte=suborder) - .exclude(id=model.id) - .reverse() - ) - else: - slice1 = new_siblings.filter(suborder__lte=suborder).exclude(id=model.id) - slice2 = ( - new_siblings.filter(suborder__gt=suborder) - .exclude(id=model.id) - .reverse() - ) - model.order = order - model.suborder = 0 - model.save() - combined = list(slice1) + [model] + list(slice2) - for i, m in enumerate(slice1): - m.suborder = i + 1 - m.save() - for i, m in enumerate(slice2): - m.suborder = len(combined) - i - m.save() - model.suborder = len(slice1) + 1 - model.save() - for i, m in enumerate(old_siblings): - m.suborder = i + 1 - m.save() - return HttpResponse( - serializers.serialize( - 'json', combined + list(old_siblings), ensure_ascii=False - ), - content_type='application/json;charset=utf-8', - ) - - -@generic_api_view -@never_cache -@require_GET -def hosts(request, event): - # this is deprecated and is getting removed as soon as wyrm can point the schedule page at the new endpoint - runs = SpeedRun.objects.filter(event=event).prefetch_related('hosts') - hosts = [] - for run in runs: - hosts.append( - { - 'model': 'tracker.hostslot', - 'pk': run.pk, # TERRIBLE LIE - 'fields': { - 'start_run': run.pk, - 'end_run': run.pk, - 'name': ', '.join(h.name for h in run.hosts.all()), - }, - } - ) - resp = HttpResponse( - json.dumps(hosts), - content_type='application/json;charset=utf-8', - ) - if 'queries' in request.GET and request.user.has_perm('tracker.view_queries'): - return HttpResponse( - json.dumps(connection.queries, ensure_ascii=False, indent=1), - content_type='application/json;charset=utf-8', - ) - return resp