# -*- coding: utf-8 -*- """API resources.""" import logging import redis from django.conf.urls import url from django.contrib.auth.models import User from django.core.cache import cache from django.shortcuts import get_object_or_404 from tastypie import fields from tastypie.authorization import DjangoAuthorization, ReadOnlyAuthorization from tastypie.constants import ALL, ALL_WITH_RELATIONS from tastypie.http import HttpCreated from tastypie.resources import ModelResource from tastypie.utils import dict_strip_unicode_keys, trailing_slash from readthedocs.builds.constants import LATEST from readthedocs.builds.models import Version from readthedocs.core.utils import trigger_build from readthedocs.projects.models import ImportedFile, Project from .utils import PostAuthentication log = logging.getLogger(__name__) class ProjectResource(ModelResource): """API resource for Project model.""" users = fields.ToManyField('readthedocs.api.base.UserResource', 'users') class Meta: include_absolute_url = True allowed_methods = ['get', 'post', 'put'] queryset = Project.objects.api() authentication = PostAuthentication() authorization = ReadOnlyAuthorization() excludes = ['path', 'featured', 'programming_language'] filtering = { 'users': ALL_WITH_RELATIONS, 'slug': ALL_WITH_RELATIONS, } def get_object_list(self, request): self._meta.queryset = Project.objects.api(user=request.user) return super().get_object_list(request) def dehydrate(self, bundle): bundle.data['downloads'] = bundle.obj.get_downloads() return bundle def post_list(self, request, **kwargs): """ Creates a new resource/object with the provided data. Calls ``obj_create`` with the provided data and returns a response with the new resource's location. If a new resource is created, return ``HttpCreated`` (201 Created). """ deserialized = self.deserialize( request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json'), ) # Force this in an ugly way, at least should do "reverse" deserialized['users'] = ['/api/v1/user/%s/' % request.user.id] bundle = self.build_bundle( data=dict_strip_unicode_keys(deserialized), request=request, ) self.is_valid(bundle) updated_bundle = self.obj_create(bundle, request=request) return HttpCreated(location=self.get_resource_uri(updated_bundle)) def prepend_urls(self): return [ url( r'^(?P%s)/schema/$' % self._meta.resource_name, self.wrap_view('get_schema'), name='api_get_schema', ), url( r'^(?P%s)/search%s$' % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_search'), name='api_get_search', ), url( (r'^(?P%s)/(?P[a-z-_]+)/$') % self._meta.resource_name, self.wrap_view('dispatch_detail'), name='api_dispatch_detail', ), ] class VersionResource(ModelResource): """API resource for Version model.""" project = fields.ForeignKey(ProjectResource, 'project', full=True) class Meta: allowed_methods = ['get', 'put', 'post'] always_return_data = True queryset = Version.objects.api() authentication = PostAuthentication() authorization = DjangoAuthorization() filtering = { 'project': ALL_WITH_RELATIONS, 'slug': ALL_WITH_RELATIONS, 'active': ALL, } def get_object_list(self, request): self._meta.queryset = Version.objects.api(user=request.user) return super().get_object_list(request) def build_version(self, request, **kwargs): project = get_object_or_404(Project, slug=kwargs['project_slug']) version = kwargs.get('version_slug', LATEST) version_obj = project.versions.get(slug=version) trigger_build(project=project, version=version_obj) return self.create_response(request, {'building': True}) def prepend_urls(self): return [ url( r'^(?P%s)/schema/$' % self._meta.resource_name, self.wrap_view('get_schema'), name='api_get_schema', ), url( r'^(?P%s)/(?P[a-z-_]+[a-z0-9-_]+)/$' # noqa % self._meta.resource_name, self.wrap_view('dispatch_list'), name='api_version_list', ), url( ( r'^(?P%s)/(?P[a-z-_]+[a-z0-9-_]+)/(?P' r'[a-z0-9-_.]+)/build/$' ) % self._meta.resource_name, self.wrap_view('build_version'), name='api_version_build_slug', ), ] class FileResource(ModelResource): """API resource for ImportedFile model.""" project = fields.ForeignKey(ProjectResource, 'project', full=True) class Meta: allowed_methods = ['get', 'post'] queryset = ImportedFile.objects.all() excludes = ['md5', 'slug'] include_absolute_url = True authentication = PostAuthentication() authorization = DjangoAuthorization() def prepend_urls(self): return [ url( r'^(?P%s)/schema/$' % self._meta.resource_name, self.wrap_view('get_schema'), name='api_get_schema', ), url( r'^(?P%s)/anchor%s$' % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_anchor'), name='api_get_anchor', ), ] def get_anchor(self, request, **__): self.method_check(request, allowed=['get']) self.is_authenticated(request) self.throttle_check(request) query = request.GET.get('q', '') try: redis_client = cache.get_client(None) redis_data = redis_client.keys('*redirects:v4*%s*' % query) except (AttributeError, redis.exceptions.ConnectionError): redis_data = [] # -2 because http: urls = [ ''.join(data.split(':')[6:]) for data in redis_data if 'http://' in data ] object_list = {'objects': urls} self.log_throttled_access(request) return self.create_response(request, object_list) class UserResource(ModelResource): """Read-only API resource for User model.""" class Meta: allowed_methods = ['get'] queryset = User.objects.all() fields = ['username', 'id'] filtering = { 'username': 'exact', } def prepend_urls(self): return [ url( r'^(?P%s)/schema/$' % self._meta.resource_name, self.wrap_view('get_schema'), name='api_get_schema', ), url( r'^(?P%s)/(?P[a-z-_]+)/$' % self._meta.resource_name, self.wrap_view('dispatch_detail'), name='api_dispatch_detail', ), ]