Skip to content

Commit

Permalink
add Interview to v2 api (read only for now) [#184870683] (#639)
Browse files Browse the repository at this point in the history
* add Interview GET to v2 api

[#184870683]

* fix azure issue with lxml>=5.0
  • Loading branch information
uraniumanchor authored Dec 29, 2023
1 parent 800147b commit 96f61d7
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 36 deletions.
46 changes: 46 additions & 0 deletions tests/apiv2/test_interviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import random

from tests import randgen
from tests.util import APITestCase
from tracker.api.serializers import InterviewSerializer


class TestInterviews(APITestCase):
model_name = 'interview'
serializer_class = InterviewSerializer
rand = random.Random()

def setUp(self):
super().setUp()
self.run = randgen.generate_run(self.rand, event=self.event, ordered=True)
self.run.save()
self.public_interview = randgen.generate_interview(self.rand, run=self.run)
self.public_interview.save()
self.private_interview = randgen.generate_interview(self.rand, run=self.run)
self.private_interview.public = False
self.private_interview.save()

def test_public_fetch(self):
data = self.get_detail(self.public_interview)
self.assertV2ModelPresent(self.public_interview, data)

def test_private_fetch(self):
self.get_detail(self.private_interview, status_code=404)

self.client.force_authenticate(self.view_user)

data = self.get_detail(self.private_interview)
self.assertV2ModelPresent(self.private_interview, data)

def test_public_list(self):
data = self.get_list()['results']
self.assertV2ModelPresent(self.public_interview, data)
self.assertV2ModelNotPresent(self.private_interview, data)

def test_private_list(self):
self.get_list(data={'all': ''}, status_code=403)

self.client.force_authenticate(self.view_user)
data = self.get_list(data={'all': ''})['results']
self.assertV2ModelPresent(self.public_interview, data)
self.assertV2ModelPresent(self.private_interview, data)
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ backports.zoneinfo==0.2.1 ; python_version<"3.9"
python-dateutil==2.8.2 ; python_version<"3.11"
webpack-manifest==2.1.1
# only for testing
lxml==4.9.4 ; python_version<"3.10" # azure issue?
responses~=0.24.1
selenium==4.16.0
tblib==3.0.0
Expand Down
41 changes: 38 additions & 3 deletions tests/test_interstitial.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,21 @@ def test_move_interstitial_fill_holes(self):
}
)

# smoke test
def test_full_schedule(self):
ad = models.Ad.objects.create(
event=self.event1, order=self.run1.order, suborder=1, sponsor_name='Yetee'
)
interview = models.Interview.objects.create(
event=self.event1, order=self.run2.order, suborder=1, interviewers='feasel'
)
self.client.force_login(self.superuser)
resp = self.client.get(
reverse('admin:view_full_schedule', args=(self.event1.pk,))
)
self.assertContains(resp, ad.sponsor_name)
self.assertContains(resp, interview.interviewers)


class TestInterview(APITestCase):
model_name = 'interview'
Expand Down Expand Up @@ -320,10 +335,28 @@ def test_private_fetch(self):
self.assertModelPresent(self.format_interview(self.public_interview), data)
self.assertModelPresent(self.format_interview(self.private_interview), data)

def test_for_run(self):
self.ad = models.Ad.objects.create(
event=self.event,
order=self.run.order,
suborder=self.private_interview.suborder + 1,
)
self.assertQuerysetEqual(
models.Interview.objects.for_run(self.run),
[self.public_interview, self.private_interview],
)


class TestAd(APITestCase):
model_name = 'ad'

def setUp(self):
super().setUp()
self.run = randgen.generate_run(self.rand, event=self.event, ordered=True)
self.run.save()
# TODO: randgen.generate_ad
self.ad = models.Ad.objects.create(event=self.event, order=1, suborder=1)

@classmethod
def format_ad(cls, ad):
return dict(
Expand All @@ -343,11 +376,13 @@ def format_ad(cls, ad):
)

def test_ads_endpoint(self):
models.SpeedRun.objects.create(event=self.event, name='Test Run 1', order=1)
ad = models.Ad.objects.create(event=self.event, order=1, suborder=1)
resp = self.client.get(reverse('tracker:api_v1:ads', args=(self.event.id,)))
self.assertEqual(resp.status_code, 403)
self.client.force_login(self.view_user)
resp = self.client.get(reverse('tracker:api_v1:ads', args=(self.event.id,)))
data = self.parseJSON(resp)
self.assertModelPresent(self.format_ad(ad), data)
self.assertModelPresent(self.format_ad(self.ad), data)

def test_for_run(self):
randgen.generate_interview(self.rand, run=self.run).save()
self.assertQuerysetEqual(models.Ad.objects.for_run(self.run), [self.ad])
11 changes: 11 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def test_nulls_removed(self):

class APITestCase(TransactionTestCase):
model_name = None
serializer_class = None
view_user_permissions = [] # trickles to add_user and locked_user
add_user_permissions = [] # trickles to locked_user
locked_user_permissions = []
Expand Down Expand Up @@ -460,6 +461,11 @@ def assertModelNotPresent(self, unexpected_model, data):
def assertV2ModelPresent(self, expected_model, data, partial=False, msg=None):
if not isinstance(data, list):
data = [data]
if not isinstance(expected_model, dict):
assert (
self.serializer_class is not None
), 'no serializer_class provided and raw model was passed'
expected_model = self.serializer_class(expected_model).data
try:
found_model = next(
m
Expand All @@ -485,6 +491,11 @@ def assertV2ModelPresent(self, expected_model, data, partial=False, msg=None):
)

def assertV2ModelNotPresent(self, unexpected_model, data):
if not isinstance(unexpected_model, dict):
assert hasattr(
self, 'serializer_class'
), 'no serializer_class provided and raw model was passed'
unexpected_model = self.serializer_class(unexpected_model).data
with self.assertRaises(
StopIteration,
msg='Found model "%s:%s" in data'
Expand Down
22 changes: 8 additions & 14 deletions tracker/admin/interstitial.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@

@admin.register(tracker.models.Ad)
class InterstitialAdmin(EventLockedMixin, admin.ModelAdmin):
class Form(forms.ModelForm):
class Meta:
exclude = ('order',)
exclude = ('order',)

class Form(forms.ModelForm):
event = AutoCompleteSelectField(
'event', initial=current_or_next_event_id, required=True
)
Expand All @@ -33,7 +32,7 @@ def __init__(self, *args, **kwargs):
self.fields['run'].initial = self.instance.run and self.instance.run.id

def clean(self):
if self.cleaned_data['run']:
if self.cleaned_data.get('run', None):
self.cleaned_data['order'] = self.cleaned_data['run'].order
self.instance.order = self.cleaned_data['run'].order
return super(InterstitialAdmin.Form, self).clean()
Expand Down Expand Up @@ -66,7 +65,7 @@ def get_urls(self):

@admin.register(tracker.models.Interview)
class InterviewAdmin(InterstitialAdmin):
exclude = ('clips',)
exclude = InterstitialAdmin.exclude + ('clips',)


@permission_required('tracker.view_interstitial')
Expand All @@ -90,16 +89,11 @@ def view_full_schedule(request, event=None):
.exclude(order=None)
)
for run in runs:
run.interstitials = tracker.models.Interstitial.interstitials_for_run(run)
run.interstitials = list(
tracker.models.Ad.objects.filter(interstitial_ptr__in=run.interstitials)
) + list(
tracker.models.Interview.objects.filter(
interstitial_ptr__in=run.interstitials
)
)
# TODO: this is horribly inefficient
run.interstitials = sorted(
run.interstitials, key=lambda i: (i.order, i.suborder)
list(tracker.models.Ad.objects.for_run(run))
+ list(tracker.models.Interview.objects.for_run(run)),
key=lambda i: (i.order, i.suborder),
)
if 'queries' in request.GET and request.user.has_perm('tracker.view_queries'):
return HttpResponse(
Expand Down
8 changes: 8 additions & 0 deletions tracker/api/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@
INVALID_FEED = _('`%s` is not a valid feed.')
INVALID_FEED_CODE = 'invalid_feed'
INVALID_SEARCH_PARAMETER_CODE = 'invalid_search_parameter'
UNAUTHORIZED_LOCKED_EVENT = _(
'You do not have permission to edit objects associated with locked events.'
)
UNAUTHORIZED_LOCKED_EVENT_CODE = 'unauthorized_locked_event'
UNAUTHORIZED_FEED = _('You do not have permission to view that feed.')
UNAUTHORIZED_FEED_CODE = 'unauthorized_feed'
UNAUTHORIZED_OBJECT = _('You do not have permission to view that object.')
UNAUTHORIZED_OBJECT_CODE = 'unauthorized_object'
34 changes: 23 additions & 11 deletions tracker/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@
from tracker.api import messages
from tracker.models import Bid

UNAUTHORIZED_LOCKED_EVENT = 'unauthorized_locked_event'
UNAUTHORIZED_FEED = 'unauthorized_feed'
UNAUTHORIZED_OBJECT = 'unauthorized_object'
UNAUTHORIZED_FIELD = 'unauthorized_field'


def tracker_permission(permission_name: str):
class TrackerPermission(BasePermission):
Expand All @@ -32,8 +27,8 @@ def has_object_permission(self, request: Request, view: t.Callable, obj: t.Any):


class EventLockedPermission(DjangoModelPermissionsOrAnonReadOnly):
message = _('You do not have permission to edit locked events.')
code = UNAUTHORIZED_LOCKED_EVENT
message = messages.UNAUTHORIZED_LOCKED_EVENT
code = messages.UNAUTHORIZED_LOCKED_EVENT_CODE

def has_permission(self, request: Request, view: t.Callable):
return super().has_permission(request, view) and (
Expand All @@ -53,7 +48,7 @@ def has_object_permission(self, request: Request, view: t.Callable, obj: t.Any):
class BidFeedPermission(BasePermission):
PUBLIC_FEEDS = Bid.PUBLIC_FEEDS
message = _('You do not have permission to view that feed.')
code = UNAUTHORIZED_FEED
code = messages.UNAUTHORIZED_FEED_CODE

def has_permission(self, request: Request, view: t.Callable):
feed = view.get_feed()
Expand All @@ -67,7 +62,7 @@ def has_permission(self, request: Request, view: t.Callable):
class BidStatePermission(BasePermission):
PUBLIC_STATES = Bid.PUBLIC_STATES
message = messages.GENERIC_NOT_FOUND
code = UNAUTHORIZED_OBJECT
code = messages.UNAUTHORIZED_OBJECT_CODE

def has_object_permission(self, request: Request, view: t.Callable, obj: t.Any):
return super().has_object_permission(request, view, obj) and (
Expand All @@ -88,12 +83,29 @@ def has_permission(self, request: Request, view: t.Callable):


class CanSendToReader(tracker_permission('tracker.change_donation')):
def has_permission(self, request, view):
return super().has_permission(request, view)
# TODO: message/code? this is -sort- of an internal use case

def has_object_permission(self, request, view, obj):
return (
super().has_object_permission(request, view, obj)
and obj.event.use_one_step_screening
or request.user.has_perm('tracker.send_to_reader')
)


class PrivateInterviewListPermission(BasePermission):
message = messages.UNAUTHORIZED_FILTER_PARAM
code = messages.UNAUTHORIZED_FILTER_PARAM_CODE

def has_permission(self, request, view):
return 'all' not in request.query_params or request.user.has_perm(
'tracker.view_interview'
)


class PrivateInterviewDetailPermission(BasePermission):
message = messages.GENERIC_NOT_FOUND
code = messages.UNAUTHORIZED_OBJECT_CODE

def has_object_permission(self, request, view, obj):
return obj.public or request.user.has_perm('tracker.view_interview')
25 changes: 25 additions & 0 deletions tracker/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from tracker.models import Interview
from tracker.models.bid import Bid, DonationBid
from tracker.models.donation import Donation, Donor
from tracker.models.event import Event, Headset, Runner, SpeedRun
Expand Down Expand Up @@ -393,3 +394,27 @@ def get_fields(self):
if not self.with_tech_notes and 'tech_notes' in fields:
del fields['tech_notes']
return fields


class InterviewSerializer(EventNestedSerializerMixin, TrackerModelSerializer):
type = ClassNameField()
event = EventSerializer()

class Meta:
model = Interview
fields = (
'type',
'id',
'event',
'order',
'suborder',
'social_media',
'interviewers',
'topic',
'public',
'prerecorded',
'producer',
'length',
'subjects',
'camera_operator',
)
6 changes: 3 additions & 3 deletions tracker/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from django.urls import include, path
from rest_framework import routers

import tracker.api.views.run
from tracker.api import views
from tracker.api.views import bids, donations, me
from tracker.api.views import bids, donations, interview, me, run

router = routers.DefaultRouter()

Expand All @@ -25,7 +24,8 @@ def event_nested_route(path, viewset, *, feed=False, **kwargs):
router.register(r'events', views.EventViewSet)
event_nested_route(r'bids', bids.BidViewSet, feed=True)
router.register(r'runners', views.RunnerViewSet)
event_nested_route(r'runs', tracker.api.views.run.SpeedRunViewSet)
event_nested_route(r'runs', run.SpeedRunViewSet)
event_nested_route(r'interviews', interview.InterviewViewSet)
router.register(r'donations', donations.DonationViewSet, basename='donations')
router.register(r'me', me.MeViewSet, basename='me')

Expand Down
9 changes: 4 additions & 5 deletions tracker/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
from rest_framework.response import Response

from tracker import logutil, settings
from tracker.api.messages import GENERIC_NOT_FOUND
from tracker.api import messages
from tracker.api.pagination import TrackerPagination
from tracker.api.permissions import UNAUTHORIZED_OBJECT
from tracker.api.serializers import EventSerializer, RunnerSerializer
from tracker.models.event import Event, Runner

Expand Down Expand Up @@ -146,9 +145,9 @@ def generic_404(exception_handler):
def _inner(exc, context):
# override the default messaging for 404s
if isinstance(exc, Http404):
exc = NotFound(detail=GENERIC_NOT_FOUND)
exc = NotFound(detail=messages.GENERIC_NOT_FOUND)
if isinstance(exc, NotFound) and exc.detail == NotFound.default_detail:
exc.detail = GENERIC_NOT_FOUND
exc.detail = messages.GENERIC_NOT_FOUND
return exception_handler(exc, context)

return _inner
Expand Down Expand Up @@ -198,7 +197,7 @@ def get_renderers(self):
]

def permission_denied(self, request, message=None, code=None):
if code == UNAUTHORIZED_OBJECT:
if code == messages.UNAUTHORIZED_OBJECT_CODE:
raise Http404
else:
super().permission_denied(request, message=message, code=code)
Expand Down
Loading

0 comments on commit 96f61d7

Please sign in to comment.