Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 95 additions & 26 deletions pages/api/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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):
Copy link
Contributor

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 in progress.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))`?

Copy link
Contributor Author

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)).

Copy link
Contributor

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 of EnumeratedProgress.extra['live_event_rank'] after it has been set once, there is some kind of warning/error being raised. Thank you!

raise Http404

serializer = self.get_serializer(progress)
return api_response.Response(
serializer.data, status=status.HTTP_200_OK)

def post(self, request, *args, **kwargs):
"""
Expand All @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The 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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method does not seem necessary as LiveEventAttendeesAPIView inherits from ListAPIView.

return super(LiveEventAttendeesAPIView, self).get(request, *args, **kwargs)

58 changes: 50 additions & 8 deletions pages/api/sequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the redefinition of ListCreateAPIView.post to add API documentation.

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)
14 changes: 14 additions & 0 deletions pages/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to serialize the extra field here (i.e. json.dumps) since the whole idea of get_extra is to unserialize it only once. Serialization should happen automatically on obj.save().

return prev_val


def update_context_urls(context, urls):
if 'urls' in context:
for key, val in six.iteritems(urls):
Expand Down
21 changes: 15 additions & 6 deletions pages/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -144,7 +145,7 @@ def element(self):

class SequenceMixin(object):
"""
Returns an ``User`` from a URL.
Returns a ``Sequence`` from a URL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent catch!

"""
sequence_url_kwarg = 'sequence'

Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -314,14 +317,20 @@ class EnumeratedProgressMixin(SequenceProgressMixin):

rank_url_kwarg = 'rank'

@property
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Loading