-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scheduling of Live Events(#77) #92
base: main
Are you sure you want to change the base?
Changes from 7 commits
6223ab8
df02270
22ff257
130ca53
95b9d9c
ac519ce
0c40bc0
df5f4e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,18 +22,22 @@ | |
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | ||
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
|
||
from datetime import timedelta | ||
import datetime | ||
|
||
from deployutils.helpers import datetime_or_now | ||
from django.shortcuts import get_object_or_404 | ||
from django.http import Http404 | ||
from rest_framework import response as api_response, status | ||
from rest_framework.generics import DestroyAPIView, ListAPIView, RetrieveAPIView | ||
from rest_framework.generics import (DestroyAPIView, ListAPIView, RetrieveAPIView) | ||
from rest_framework.mixins import DestroyModelMixin | ||
|
||
from .. import settings | ||
from ..docs import extend_schema | ||
from ..mixins import EnumeratedProgressMixin, SequenceProgressMixin | ||
from ..helpers import get_extra, set_extra | ||
from ..mixins import EnumeratedProgressMixin, SequenceProgressMixin, SequenceMixin | ||
from ..models import EnumeratedElements, EnumeratedProgress, LiveEvent | ||
from ..serializers import (EnumeratedProgressSerializer, | ||
AttendanceInputSerializer) | ||
LiveEventAttendeesSerializer) | ||
|
||
|
||
class EnumeratedProgressListAPIView(SequenceProgressMixin, ListAPIView): | ||
|
@@ -101,8 +105,8 @@ def paginate_queryset(self, queryset): | |
results = page if page else queryset | ||
for elem in results: | ||
if (elem.viewing_duration is not None and | ||
not isinstance(elem.viewing_duration, timedelta)): | ||
elem.viewing_duration = timedelta( | ||
not isinstance(elem.viewing_duration, datetime.timedelta)): | ||
elem.viewing_duration = datetime.timedelta( | ||
microseconds=elem.viewing_duration) | ||
return results | ||
|
||
|
@@ -187,21 +191,20 @@ def post(self, request, *args, **kwargs): | |
if instance.last_ping_time: | ||
time_elapsed = now - instance.last_ping_time | ||
# Add only the actual time elapsed, with a cap for inactivity | ||
time_increment = min(time_elapsed, timedelta(seconds=settings.PING_INTERVAL+1)) | ||
time_increment = min(time_elapsed, datetime.timedelta(seconds=settings.PING_INTERVAL+1)) | ||
else: | ||
# Set the initial increment to the expected ping interval (i.e., 10 seconds) | ||
time_increment = timedelta(seconds=settings.PING_INTERVAL) | ||
time_increment = datetime.timedelta(seconds=settings.PING_INTERVAL) | ||
|
||
instance.viewing_duration += time_increment | ||
instance.last_ping_time = now | ||
instance.save() | ||
|
||
status_code = status.HTTP_200_OK | ||
serializer = self.get_serializer(instance) | ||
return api_response.Response(serializer.data, status=status_code) | ||
return api_response.Response(serializer.data, status=status.HTTP_200_OK) | ||
|
||
|
||
class LiveEventAttendanceAPIView(EnumeratedProgressRetrieveAPIView): | ||
class LiveEventAttendanceAPIView(DestroyModelMixin, EnumeratedProgressRetrieveAPIView): | ||
""" | ||
Retrieves attendance to live event | ||
|
||
|
@@ -224,11 +227,22 @@ class LiveEventAttendanceAPIView(EnumeratedProgressRetrieveAPIView): | |
} | ||
""" | ||
rank_url_kwarg = 'rank' | ||
event_rank_kwarg = 'event_rank' | ||
|
||
def get_serializer_class(self): | ||
if self.request.method.lower() == 'post': | ||
return AttendanceInputSerializer | ||
return super(LiveEventAttendanceAPIView, self).get_serializer_class() | ||
serializer_class = EnumeratedProgressSerializer | ||
|
||
def get(self, request, *args, **kwargs): | ||
# Need to ensure the EnumeratedProgress matches the correct LiveEvent | ||
# As it currently returns any EnumeratedProgress on the PageElement. | ||
progress = self.progress | ||
live_event_rank = get_extra(progress, "live_event_rank") | ||
|
||
if live_event_rank != self.kwargs.get(self.event_rank_kwarg): | ||
raise Http404 | ||
|
||
serializer = self.get_serializer(progress) | ||
return api_response.Response( | ||
serializer.data, status=status.HTTP_200_OK) | ||
|
||
def post(self, request, *args, **kwargs): | ||
""" | ||
|
@@ -253,21 +267,76 @@ def post(self, request, *args, **kwargs): | |
"detail": "Attendance marked successfully" | ||
} | ||
""" | ||
serializer = self.get_serializer(data=request.data) | ||
serializer.is_valid(raise_exception=True) | ||
|
||
at_time = datetime_or_now() | ||
event_rank = kwargs.get(self.event_rank_kwarg) | ||
progress = self.get_object() | ||
element = progress.step | ||
live_event = LiveEvent.objects.filter(element=element.content).first() | ||
|
||
# We use if live_event to confirm the existence of the LiveEvent object | ||
if (live_event and | ||
progress.viewing_duration <= element.min_viewing_duration): | ||
progress.viewing_duration = element.min_viewing_duration | ||
live_event = get_object_or_404( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the code I would have expected in previous comment. |
||
LiveEvent, element=element.content, rank=event_rank) | ||
|
||
progress_event_rank = set_extra( | ||
progress, "live_event_rank", event_rank) | ||
|
||
if progress.viewing_duration < element.min_viewing_duration and live_event.scheduled_at < at_time: | ||
if progress_event_rank and progress_event_rank != event_rank: | ||
return api_response.Response( | ||
{'detail': f'Attendance already marked for Live Event ' | ||
f'with rank: {event_rank}'}, | ||
status=status.HTTP_400_BAD_REQUEST) | ||
progress.viewing_duration = max( | ||
progress.viewing_duration, element.min_viewing_duration) | ||
progress.save() | ||
serializer = self.get_serializer(progress) | ||
return api_response.Response( | ||
{'detail': 'Attendance marked successfully'}, | ||
status=status.HTTP_200_OK) | ||
serializer.data, status=status.HTTP_201_CREATED) | ||
|
||
return api_response.Response( | ||
{'detail': 'Attendance not marked'}, | ||
status=status.HTTP_400_BAD_REQUEST) | ||
|
||
def delete(self, request, *args, **kwargs): | ||
''' | ||
Resets Live Event Attendance | ||
''' | ||
# Maybe redundant because we're already resetting EnumeratedProgress | ||
# in EnumeratedProgressResetAPIView? | ||
event_rank = kwargs.get(self.event_rank_kwarg) | ||
progress = self.get_object() | ||
|
||
curr_val = set_extra( | ||
progress, "live_event_rank", '') | ||
|
||
if curr_val != event_rank: | ||
return api_response.Response( | ||
status=status.HTTP_400_BAD_REQUEST) | ||
|
||
progress.viewing_duration = datetime.timedelta(seconds=0) | ||
progress.save() | ||
return api_response.Response(status=status.HTTP_204_NO_CONTENT) | ||
|
||
|
||
class LiveEventAttendeesAPIView(SequenceMixin, ListAPIView): | ||
serializer_class = LiveEventAttendeesSerializer | ||
|
||
rank_url_kwarg = 'rank' | ||
event_rank_kwarg = 'event_rank' | ||
|
||
def get_queryset(self): | ||
element_rank = self.kwargs.get(self.rank_url_kwarg) | ||
event_rank = self.kwargs.get(self.event_rank_kwarg) | ||
|
||
element = get_object_or_404( | ||
EnumeratedElements, sequence=self.sequence, rank=element_rank) | ||
|
||
queryset = EnumeratedProgress.objects.filter( | ||
step__content=element.content, | ||
viewing_duration__gte=element.min_viewing_duration) | ||
|
||
progress_ids = [progress.id for progress in queryset if | ||
get_extra(progress, "live_event_rank") == event_rank] | ||
queryset = queryset.filter(id__in=progress_ids) | ||
return queryset | ||
|
||
def get(self, request, *args, **kwargs): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method does not seem necessary as |
||
return super(LiveEventAttendeesAPIView, self).get(request, *args, **kwargs) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,19 +21,22 @@ | |
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR | ||
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | ||
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
import dateutil.parser | ||
import logging | ||
|
||
from django.db import transaction, IntegrityError | ||
from django.template.defaultfilters import slugify | ||
from rest_framework import response as api_response, status | ||
from rest_framework.filters import OrderingFilter, SearchFilter | ||
from rest_framework.generics import (get_object_or_404, DestroyAPIView, | ||
ListAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView) | ||
ListAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView, CreateAPIView) | ||
from rest_framework.views import APIView | ||
|
||
from ..mixins import AccountMixin, SequenceMixin | ||
from ..models import Sequence, EnumeratedElements | ||
from ..mixins import AccountMixin, SequenceMixin, PageElementMixin, EnumeratedProgressMixin | ||
from ..models import Sequence, EnumeratedElements, PageElement, LiveEvent | ||
from ..serializers import (SequenceSerializer, SequenceCreateSerializer, | ||
EnumeratedElementSerializer) | ||
EnumeratedElementSerializer, LiveEventSerializer) | ||
|
||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
@@ -401,10 +404,6 @@ def post(self, request, *args, **kwargs): | |
{'detail': str(err)}, | ||
status=status.HTTP_400_BAD_REQUEST) | ||
|
||
return api_response.Response( | ||
serializer.errors, | ||
status=status.HTTP_400_BAD_REQUEST) | ||
|
||
|
||
class RemoveElementFromSequenceAPIView(AccountMixin, SequenceMixin, | ||
DestroyAPIView): | ||
|
@@ -432,3 +431,46 @@ def perform_destroy(self, instance): | |
self.sequence.has_certificate = False | ||
self.sequence.save() | ||
instance.delete() | ||
|
||
|
||
class LiveEventListCreateAPIView(AccountMixin, PageElementMixin, ListCreateAPIView): | ||
''' | ||
Lists Live Events belonging to a Page Element | ||
''' | ||
serializer_class = LiveEventSerializer | ||
|
||
def get_queryset(self): | ||
queryset = LiveEvent.objects.filter( | ||
element=self.element | ||
).order_by('-status', 'rank') | ||
|
||
return queryset | ||
|
||
def get(self, request, *args, **kwargs): | ||
return super(LiveEventListCreateAPIView, self).get(request, *args, **kwargs) | ||
|
||
def post(self, request, *args, **kwargs): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the redefinition of |
||
return super(LiveEventListCreateAPIView, self).post(request, *args, **kwargs) | ||
|
||
def perform_create(self, serializer): | ||
serializer.save(element=self.element, status=LiveEvent.SCHEDULED) | ||
|
||
|
||
class LiveEventRetrieveUpdateDestroyAPIView(PageElementMixin, RetrieveUpdateDestroyAPIView): | ||
''' | ||
Retrieve a LiveEvent | ||
''' | ||
serializer_class = LiveEventSerializer | ||
# Doesn't allow editing the Status field because it is a read_only | ||
# field in the Serializer since we're using the Delete method | ||
# to set it. | ||
|
||
def get_object(self): | ||
return get_object_or_404(LiveEvent.objects.all(), | ||
element=self.element, rank=self.kwargs.get('rank')) | ||
|
||
def delete(self, request, *args, **kwargs): | ||
live_event = self.get_object() | ||
live_event.status = LiveEvent.CANCELLED | ||
live_event.save() | ||
return api_response.Response(status=status.HTTP_204_NO_CONTENT) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,20 @@ def get_extra(obj, attr_name, default=None): | |
extra = obj.get('extra') | ||
return extra.get(attr_name, default) if extra else default | ||
|
||
|
||
def set_extra(obj, attr_name, attr_value): | ||
prev_val = get_extra(obj, attr_name) | ||
|
||
if not obj.extra: | ||
# In case we have a None type or empty string, initialize | ||
# extra as a dict because if we're using this method the intention | ||
# is to set it to some value. | ||
obj.extra = {} | ||
obj.extra.update({attr_name: attr_value}) | ||
obj.extra = json.dumps(obj.extra) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't want to serialize the extra field here (i.e. |
||
return prev_val | ||
|
||
|
||
def update_context_urls(context, urls): | ||
if 'urls' in context: | ||
for key, val in six.iteritems(urls): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ | |
from django.contrib.auth import get_user_model | ||
from django.core.exceptions import ImproperlyConfigured | ||
from django.db import transaction | ||
from django.db.models import F | ||
from django.http import Http404 | ||
from rest_framework.generics import get_object_or_404 | ||
|
||
|
@@ -144,7 +145,7 @@ def element(self): | |
|
||
class SequenceMixin(object): | ||
""" | ||
Returns an ``User`` from a URL. | ||
Returns a ``Sequence`` from a URL. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent catch! |
||
""" | ||
sequence_url_kwarg = 'sequence' | ||
|
||
|
@@ -283,7 +284,7 @@ def update_element(self, obj): | |
obj.title = obj.content.title | ||
obj.url = reverse('sequence_page_element_view', | ||
args=(self.user, self.sequence, obj.rank)) | ||
obj.is_live_event = obj.content.slug in self.live_events | ||
obj.is_live_event = obj.content.slug in self.live_events.values_list('element_slug', flat=True) | ||
obj.is_certificate = (obj.rank == self.last_rank_element.rank) if \ | ||
self.last_rank_element else False | ||
|
||
|
@@ -296,8 +297,10 @@ def get_queryset(self): | |
|
||
def decorate_queryset(self, queryset): | ||
self.live_events = LiveEvent.objects.filter( | ||
element__in=[obj.content for obj in queryset] | ||
).values_list('element__slug', flat=True) | ||
element__in=[ | ||
obj.content for obj in queryset]).order_by( | ||
'scheduled_at').annotate( | ||
element_slug=F('element__slug')) | ||
|
||
self.last_rank_element = None | ||
if self.sequence.has_certificate: | ||
|
@@ -314,14 +317,20 @@ class EnumeratedProgressMixin(SequenceProgressMixin): | |
|
||
rank_url_kwarg = 'rank' | ||
|
||
@property | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great refactor! |
||
def rank(self): | ||
if not hasattr(self, '_rank'): | ||
self._rank = self.kwargs.get(self.rank_url_kwarg, 1) | ||
return self._rank | ||
|
||
@property | ||
def progress(self): | ||
if not hasattr(self, '_progress'): | ||
step = get_object_or_404(EnumeratedElements.objects.all(), | ||
sequence=self.sequence, | ||
rank=self.kwargs.get(self.rank_url_kwarg, 1)) | ||
rank=self.rank) | ||
with transaction.atomic(): | ||
self._progress, _ = EnumeratedProgress.objects.get_or_create( | ||
self._progress, _ = EnumeratedProgress.objects.get_or_create( | ||
sequence_progress=self.sequence_progress, | ||
step=step) | ||
return self._progress |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this code. Why is there a
live_event_rank
inprogress.extra
? Couldn't the code just be something like get_object_or_404(self.progress.live_events, rank=self.kwargs.get(self.event_rank_kwarg))`?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea here was to check that we're retrieving the correct EnumeratedProgress object, ie. one whose 'live_event_rank' in the extra field matches the live event rank in the URL, if it exists.
For example, one page element has 2 live events(ranks 1 and 2).
We access the URL to retrieve it, api_mark_attendance(args=page element, live event(rank=1))
If we retrieve the progress for that page element, this logic here returns it only if the enumeratedprogress's extra field contains live event(rank=1), meaning progress exists for that specific live event, and wouldn't return anything for api_mark_attendance(args=page element, live event(rank=2)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Please add a comment along those lines where the
EnumeratedProgress.extra
field is updated, and make sure if we try to change the value ofEnumeratedProgress.extra['live_event_rank']
after it has been set once, there is some kind of warning/error being raised. Thank you!