class APIView(View): @method_decorator(csrf_exempt) @method_decorator(require_valid_user) @method_decorator(check_username) @method_decorator(never_cache) @method_decorator(cors_origin()) def dispatch(self, *args, **kwargs): """ Dispatches request and does generic error handling """ try: return super(APIView, self).dispatch(*args, **kwargs) except ObjectDoesNotExist as e: return HttpResponseNotFound(str(e)) except (RequestException, ParameterMissing) as e: return HttpResponseBadRequest(str(e)) def parsed_body(self, request): """ Returns the object parsed from the JSON request body """ if not request.body: raise RequestException('POST data must not be empty') try: # TODO: implementation of parse_request_body can be moved here # after all views using it have been refactored return parse_request_body(request) except (UnicodeDecodeError, ValueError) as e: msg = 'Could not decode request body for user {}: {}'.format( request.user.username, request.body.decode('ascii', errors='replace')) logger.warn(msg, exc_info=True) raise RequestException(msg) def get_since(self, request): """ Returns parsed "since" GET parameter """ since_ = request.GET.get('since', None) if since_ is None: raise RequestException("parameter 'since' missing") try: since_ = int(since_) since = datetime.fromtimestamp(since_) except ValueError: raise RequestException("'since' is not a valid timestamp") if since_ < 0: raise RequestException("'since' must be a non-negative number") return since
class DeviceUpdates(View): """returns various updates for a device https://gpoddernet.readthedocs.io/en/latest/api//Devices#Get_Updates""" @method_decorator(csrf_exempt) @method_decorator(require_valid_user) @method_decorator(check_username) @method_decorator(never_cache) @method_decorator(cors_origin()) def get(self, request, username, device_uid): now = datetime.utcnow() now_ = get_timestamp(now) user = request.user try: device = user.client_set.get(uid=device_uid) except Client.DoesNotExist as e: return HttpResponseNotFound(str(e)) try: since = self.get_since(request) except ValueError as e: return HttpResponseBadRequest(str(e)) include_actions = parse_bool(request.GET.get("include_actions", False)) domain = RequestSite(request).domain add, rem, subscriptions = self.get_subscription_changes( user, device, since, now, domain) updates = self.get_episode_changes(user, subscriptions, domain, include_actions, since) return JsonResponse({ "add": add, "rem": rem, "updates": updates, "timestamp": get_timestamp(now), }) def get_subscription_changes(self, user, device, since, now, domain): """gets new, removed and current subscriptions""" history = get_subscription_history(user, device, since, now) add, rem = subscription_diff(history) subscriptions = device.get_subscribed_podcasts() add = [podcast_data(p, domain) for p in add] rem = [p.url for p in rem] return add, rem, subscriptions def get_episode_changes(self, user, subscriptions, domain, include_actions, since): devices = {dev.id.hex: dev.uid for dev in user.client_set.all()} # index subscribed podcasts by their Id for fast access podcasts = {p.get_id(): p for p in subscriptions} episode_updates = self.get_episode_updates(user, subscriptions, since) return [ self.get_episode_data(status, podcasts, domain, include_actions, user, devices) for status in episode_updates ] def get_episode_updates(self, user, subscribed_podcasts, since, max_per_podcast=5): """Returns the episode updates since the timestamp""" episodes = [] for podcast in subscribed_podcasts: eps = Episode.objects.filter(podcast=podcast, released__gt=since).order_by( "-order", "-released") episodes.extend(eps[:max_per_podcast]) states = EpisodeState.dict_for_user(user, episodes) for episode in episodes: yield EpisodeStatus(episode, states.get(episode.id, "new"), None) def get_episode_data(self, episode_status, podcasts, domain, include_actions, user, devices): """Get episode data for an episode status object""" # TODO: shouldn't the podcast_id be in the episode status? podcast_id = episode_status.episode.podcast podcast = podcasts.get(podcast_id, None) t = episode_data(episode_status.episode, domain, podcast) t["status"] = episode_status.status # include latest action (bug 1419) # TODO if include_actions and episode_status.action: t["action"] = episode_action_json(episode_status.action, user) return t def get_since(self, request): """parses the "since" parameter""" since_ = request.GET.get("since", None) if since_ is None: raise ValueError("parameter since missing") try: return datetime.fromtimestamp(float(since_)) except ValueError as e: raise ValueError("'since' is not a valid timestamp: %s" % str(e))