From dc96c6d4a72359745d36561196169d71babd1620 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 11 Dec 2017 21:13:20 -0500 Subject: [PATCH] A lot of styling/autolinting --- readthedocs/api/base.py | 143 +++++++------ readthedocs/bookmarks/views.py | 74 +++---- readthedocs/comments/views.py | 64 +++--- readthedocs/core/views/__init__.py | 27 ++- readthedocs/core/views/serve.py | 75 ++++--- readthedocs/gold/views.py | 46 +++-- readthedocs/profiles/views.py | 44 ++-- readthedocs/projects/views/private.py | 285 +++++++++++++++----------- readthedocs/projects/views/public.py | 207 +++++++++++-------- readthedocs/search/views.py | 49 +++-- 10 files changed, 573 insertions(+), 441 deletions(-) diff --git a/readthedocs/api/base.py b/readthedocs/api/base.py index 7f1211edb..a294dfb64 100644 --- a/readthedocs/api/base.py +++ b/readthedocs/api/base.py @@ -1,28 +1,30 @@ -"""API resources""" -from __future__ import absolute_import -from builtins import object -import logging +# -*- coding: utf-8 -*- +"""API resources.""" +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + import json +import logging +from builtins import object + import redis - -from django.contrib.auth.models import User from django.conf.urls import url -from django.shortcuts import get_object_or_404 +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 -from tastypie.constants import ALL_WITH_RELATIONS, ALL +from tastypie.constants import ALL, ALL_WITH_RELATIONS +from tastypie.http import HttpApplicationError, HttpCreated from tastypie.resources import ModelResource -from tastypie.http import HttpCreated, HttpApplicationError 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 Project, ImportedFile +from readthedocs.projects.models import ImportedFile, Project -from .utils import SearchMixin, PostAuthentication +from .utils import PostAuthentication, SearchMixin log = logging.getLogger(__name__) @@ -41,8 +43,8 @@ class ProjectResource(ModelResource, SearchMixin): authorization = DjangoAuthorization() excludes = ['path', 'featured', 'programming_language'] filtering = { - "users": ALL_WITH_RELATIONS, - "slug": ALL_WITH_RELATIONS, + 'users': ALL_WITH_RELATIONS, + 'slug': ALL_WITH_RELATIONS, } def get_object_list(self, request): @@ -63,28 +65,32 @@ class ProjectResource(ModelResource, SearchMixin): If a new resource is created, return ``HttpCreated`` (201 Created). """ deserialized = self.deserialize( - request, request.body, - format=request.META.get('CONTENT_TYPE', 'application/json') + 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) + 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 sync_versions(self, request, **kwargs): """ - Sync the version data in the repo (on the build server) with what we have in the database. + Sync the version data in the repo (on the build server) with what we + have in the database. Returns the identifiers for the versions that have been deleted. """ project = get_object_or_404(Project, pk=kwargs['pk']) try: post_data = self.deserialize( - request, request.body, - format=request.META.get('CONTENT_TYPE', 'application/json') + request, + request.body, + format=request.META.get('CONTENT_TYPE', 'application/json'), ) data = json.loads(post_data) self.method_check(request, allowed=['post']) @@ -104,17 +110,20 @@ class ProjectResource(ModelResource, SearchMixin): 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\d+)/sync_versions%s$" % ( - self._meta.resource_name, trailing_slash()), - self.wrap_view('sync_versions'), name="api_sync_versions"), - url((r"^(?P%s)/(?P[a-z-_]+)/$") - % self._meta.resource_name, self.wrap_view('dispatch_detail'), - name="api_dispatch_detail"), + 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\d+)/sync_versions%s$' % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('sync_versions'), name='api_sync_versions'), + url((r'^(?P%s)/(?P[a-z-_]+)/$') % + self._meta.resource_name, self.wrap_view('dispatch_detail'), + name='api_dispatch_detail'), ] @@ -131,9 +140,9 @@ class VersionResource(ModelResource): authentication = PostAuthentication() authorization = DjangoAuthorization() filtering = { - "project": ALL_WITH_RELATIONS, - "slug": ALL_WITH_RELATIONS, - "active": ALL, + 'project': ALL_WITH_RELATIONS, + 'slug': ALL_WITH_RELATIONS, + 'active': ALL, } def get_object_list(self, request): @@ -149,19 +158,19 @@ class VersionResource(ModelResource): 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 + 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"), + 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'), ] @@ -182,18 +191,17 @@ class FileResource(ModelResource, SearchMixin): 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$" % + 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)/anchor%s$" % + self.wrap_view('get_search'), name='api_get_search'), + url( + r'^(?P%s)/anchor%s$' % (self._meta.resource_name, trailing_slash()), - self.wrap_view('get_anchor'), - name="api_get_anchor"), + self.wrap_view('get_anchor'), name='api_get_anchor'), ] def get_anchor(self, request, **__): @@ -204,12 +212,14 @@ class FileResource(ModelResource, SearchMixin): query = request.GET.get('q', '') try: redis_client = cache.get_client(None) - redis_data = redis_client.keys("*redirects:v4*%s*" % query) + 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] + urls = [ + ''.join(data.split(':')[6:]) for data in redis_data + if 'http://' in data + ] object_list = {'objects': urls} self.log_throttled_access(request) @@ -230,12 +240,11 @@ class UserResource(ModelResource): 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"), + 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'), ] diff --git a/readthedocs/bookmarks/views.py b/readthedocs/bookmarks/views.py index 71acbdd19..9a790c3ff 100644 --- a/readthedocs/bookmarks/views.py +++ b/readthedocs/bookmarks/views.py @@ -1,21 +1,24 @@ +# -*- coding: utf-8 -*- """Views for the bookmarks app.""" -from __future__ import absolute_import -from django.contrib.auth.decorators import login_required -from django.http import HttpResponse, HttpResponseRedirect -from django.http import HttpResponseBadRequest -from django.shortcuts import get_object_or_404, render -from django.views.generic import ListView, View -from django.core.urlresolvers import reverse -from django.utils.decorators import method_decorator -from django.core.exceptions import ObjectDoesNotExist -from django.views.decorators.csrf import csrf_exempt +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + import json +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.http import ( + HttpResponse, HttpResponseBadRequest, HttpResponseRedirect) +from django.shortcuts import get_object_or_404, render +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import ListView, View + from readthedocs.bookmarks.models import Bookmark from readthedocs.projects.models import Project - # These views are CSRF exempt because of Django's CSRF middleware failing here # https://github.com/django/django/blob/stable/1.6.x/django/middleware/csrf.py#L135-L159 # We don't have a valid referrer because we're on a subdomain @@ -25,24 +28,23 @@ class BookmarkExistsView(View): @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): - return super(BookmarkExistsView, self).dispatch(request, *args, **kwargs) + return super(BookmarkExistsView, + self).dispatch(request, *args, **kwargs) def get(self, request): return HttpResponse( - content=json.dumps( - {'error': 'You must POST!'} - ), + content=json.dumps({'error': 'You must POST!'}), content_type='application/json', - status=405 + status=405, ) def post(self, request, *args, **kwargs): """ Returns: - 200 response with exists = True in json if bookmark exists. - 404 with exists = False in json if no matching bookmark is found. - 400 if json data is missing any one of: project, version, page. + - 200 response with exists = True in json if bookmark exists. + - 404 with exists = False in json if no matching bookmark is found. + - 400 if json data is missing any one of: project, version, page. """ post_json = json.loads(request.body) try: @@ -51,31 +53,31 @@ class BookmarkExistsView(View): page = post_json['page'] except KeyError: return HttpResponseBadRequest( - content=json.dumps({'error': 'Invalid parameters'}) + content=json.dumps({'error': 'Invalid parameters'}), ) try: Bookmark.objects.get( project__slug=project, version__slug=version, - page=page + page=page, ) except ObjectDoesNotExist: return HttpResponse( content=json.dumps({'exists': False}), status=404, - content_type="application/json" + content_type='application/json', ) return HttpResponse( content=json.dumps({'exists': True}), status=200, - content_type="application/json" + content_type='application/json', ) class BookmarkListView(ListView): - """Displays all of a logged-in user's bookmarks""" + """Displays all of a logged-in user's bookmarks.""" model = Bookmark @@ -89,7 +91,7 @@ class BookmarkListView(ListView): class BookmarkAddView(View): - """Adds bookmarks in response to POST requests""" + """Adds bookmarks in response to POST requests.""" @method_decorator(login_required) @method_decorator(csrf_exempt) @@ -98,11 +100,9 @@ class BookmarkAddView(View): def get(self, request): return HttpResponse( - content=json.dumps( - {'error': 'You must POST!'} - ), + content=json.dumps({'error': 'You must POST!'}), content_type='application/json', - status=405 + status=405, ) def post(self, request, *args, **kwargs): @@ -119,7 +119,7 @@ class BookmarkAddView(View): url = post_json['url'] except KeyError: return HttpResponseBadRequest( - content=json.dumps({'error': "Invalid parameters"}) + content=json.dumps({'error': 'Invalid parameters'}), ) try: @@ -128,8 +128,7 @@ class BookmarkAddView(View): except ObjectDoesNotExist: return HttpResponseBadRequest( content=json.dumps( - {'error': "Project or Version does not exist"} - ) + {'error': 'Project or Version does not exist'}), ) Bookmark.objects.get_or_create( @@ -142,7 +141,7 @@ class BookmarkAddView(View): return HttpResponse( json.dumps({'added': True}), status=201, - content_type='application/json' + content_type='application/json', ) @@ -157,7 +156,8 @@ class BookmarkRemoveView(View): @method_decorator(login_required) @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): - return super(BookmarkRemoveView, self).dispatch(request, *args, **kwargs) + return super(BookmarkRemoveView, + self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): return render(request, 'bookmarks/bookmark_delete.html') @@ -180,7 +180,7 @@ class BookmarkRemoveView(View): page = post_json['page'] except KeyError: return HttpResponseBadRequest( - json.dumps({'error': "Invalid parameters"}) + json.dumps({'error': 'Invalid parameters'}), ) bookmark = get_object_or_404( @@ -189,12 +189,12 @@ class BookmarkRemoveView(View): url=url, project=project, version=version, - page=page + page=page, ) bookmark.delete() return HttpResponse( json.dumps({'removed': True}), status=200, - content_type="application/json" + content_type='application/json', ) diff --git a/readthedocs/comments/views.py b/readthedocs/comments/views.py index 1cef4a7c9..6c4a807fe 100644 --- a/readthedocs/comments/views.py +++ b/readthedocs/comments/views.py @@ -1,17 +1,16 @@ +# -*- coding: utf-8 -*- """Views for comments app.""" -from __future__ import absolute_import +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + from django.contrib.auth.decorators import login_required from django.shortcuts import render from django.utils.decorators import method_decorator from rest_framework import permissions, status from rest_framework.decorators import ( - api_view, - authentication_classes, - permission_classes, - renderer_classes, - detail_route -) + api_view, authentication_classes, detail_route, permission_classes, + renderer_classes) from rest_framework.exceptions import ParseError from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -19,13 +18,14 @@ from rest_framework.viewsets import ModelViewSet from sphinx.websupport import WebSupport from readthedocs.comments.models import ( - DocumentComment, DocumentNode, NodeSnapshot, DocumentCommentSerializer, - DocumentNodeSerializer, ModerationActionSerializer) + DocumentComment, DocumentCommentSerializer, DocumentNode, + DocumentNodeSerializer, ModerationActionSerializer, NodeSnapshot) from readthedocs.projects.models import Project from readthedocs.restapi.permissions import CommentModeratorOrReadOnly from .backend import DjangoStorage from .session import UnsafeSessionAuthentication + storage = DjangoStorage() support = WebSupport( @@ -36,11 +36,11 @@ support = WebSupport( docroot='websupport', ) - ######## # called by javascript ######## + @api_view(['GET']) @permission_classes([permissions.IsAuthenticatedOrReadOnly]) @renderer_classes((JSONRenderer,)) @@ -56,7 +56,7 @@ def get_options(request): # pylint: disable=unused-argument @renderer_classes((JSONRenderer,)) def get_metadata(request): """ - Check for get_metadata + Check for get_metadata. GET: page """ @@ -84,6 +84,7 @@ def attach_comment(request): # Normal Views ####### + def build(request): # pylint: disable=unused-argument support.build() @@ -93,6 +94,7 @@ def serve_file(request, file): # pylint: disable=redefined-builtin return render(request, 'doc.html', {'document': document}) + ###### # Called by Builder ###### @@ -147,8 +149,9 @@ def update_node(request): node.update_hash(new_hash, commit) return Response(DocumentNodeSerializer(node).data) except KeyError: - return Response("You must include new_hash and commit in POST payload to this view.", - status.HTTP_400_BAD_REQUEST) + return Response( + 'You must include new_hash and commit in POST payload to this view.', + status.HTTP_400_BAD_REQUEST) class CommentViewSet(ModelViewSet): @@ -156,16 +159,21 @@ class CommentViewSet(ModelViewSet): """Viewset for Comment model.""" serializer_class = DocumentCommentSerializer - permission_classes = [CommentModeratorOrReadOnly, permissions.IsAuthenticatedOrReadOnly] + permission_classes = [ + CommentModeratorOrReadOnly, + permissions.IsAuthenticatedOrReadOnly, + ] def get_queryset(self): qp = self.request.query_params if qp.get('node'): try: - node = DocumentNode.objects.from_hash(version_slug=qp['version'], - page=qp['document_page'], - node_hash=qp['node'], - project_slug=qp['project']) + node = DocumentNode.objects.from_hash( + version_slug=qp['version'], + page=qp['document_page'], + node_hash=qp['node'], + project_slug=qp['project'], + ) queryset = DocumentComment.objects.filter(node=node) except KeyError: @@ -175,7 +183,8 @@ class CommentViewSet(ModelViewSet): except DocumentNode.DoesNotExist: queryset = DocumentComment.objects.none() elif qp.get('project'): - queryset = DocumentComment.objects.filter(node__project__slug=qp['project']) + queryset = DocumentComment.objects.filter( + node__project__slug=qp['project']) else: queryset = DocumentComment.objects.all() @@ -184,16 +193,19 @@ class CommentViewSet(ModelViewSet): @method_decorator(login_required) def create(self, request, *args, **kwargs): project = Project.objects.get(slug=request.data['project']) - comment = project.add_comment(version_slug=request.data['version'], - page=request.data['document_page'], - content_hash=request.data['node'], - commit=request.data['commit'], - user=request.user, - text=request.data['text']) + comment = project.add_comment( + version_slug=request.data['version'], + page=request.data['document_page'], + content_hash=request.data['node'], + commit=request.data['commit'], + user=request.user, + text=request.data['text'], + ) serializer = self.get_serializer(comment) headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers) @detail_route(methods=['put']) def moderate(self, request, pk): # pylint: disable=unused-argument diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index a6a5b0c05..50c82533c 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Core views, including the main homepage, @@ -10,7 +11,6 @@ from past.utils import old_div import os import logging - from django.conf import settings from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render, get_object_or_404, redirect @@ -37,7 +37,7 @@ class HomepageView(TemplateView): template_name = 'homepage.html' def get_context_data(self, **kwargs): - """Add latest builds and featured projects""" + """Add latest builds and featured projects.""" context = super(HomepageView, self).get_context_data(**kwargs) latest = [] latest_builds = ( @@ -47,7 +47,7 @@ class HomepageView(TemplateView): success=True, ) .order_by('-date') - )[:100] + )[:100] # yapf: disable for build in latest_builds: if (build.project not in latest and len(latest) < 10): latest.append(build.project) @@ -64,7 +64,8 @@ class SupportView(TemplateView): support_email = getattr(settings, 'SUPPORT_EMAIL', None) if not support_email: support_email = 'support@{domain}'.format( - domain=getattr(settings, 'PRODUCTION_DOMAIN', 'readthedocs.org')) + domain=getattr( + settings, 'PRODUCTION_DOMAIN', 'readthedocs.org')) context['support_email'] = support_email return context @@ -83,10 +84,13 @@ def random_page(request, project_slug=None): # pylint: disable=unused-argument @csrf_exempt def wipe_version(request, project_slug, version_slug): - version = get_object_or_404(Version, project__slug=project_slug, - slug=version_slug) + version = get_object_or_404( + Version, + project__slug=project_slug, + slug=version_slug, + ) if request.user not in version.project.users.all(): - raise Http404("You must own this project to wipe it.") + raise Http404('You must own this project to wipe it.') if request.method == 'POST': del_dirs = [ @@ -97,8 +101,9 @@ def wipe_version(request, project_slug, version_slug): for del_dir in del_dirs: broadcast(type='build', task=remove_dir, args=[del_dir]) return redirect('project_version_list', project_slug) - return render(request, 'wipe_version.html', - {'version': version, 'project': version.project}) + return render( + request, 'wipe_version.html', + {'version': version, 'project': version.project}) def divide_by_zero(request): # pylint: disable=unused-argument @@ -106,14 +111,14 @@ def divide_by_zero(request): # pylint: disable=unused-argument def server_error_500(request, template_name='500.html'): - """A simple 500 handler so we get media""" + """A simple 500 handler so we get media.""" r = render(request, template_name) r.status_code = 500 return r def server_error_404(request, exception, template_name='404.html'): # pylint: disable=unused-argument # noqa - """A simple 404 handler so we get media""" + """A simple 404 handler so we get media.""" response = get_redirect_response(request, path=request.get_full_path()) if response: return response diff --git a/readthedocs/core/views/serve.py b/readthedocs/core/views/serve.py index 895515499..1f0737235 100644 --- a/readthedocs/core/views/serve.py +++ b/readthedocs/core/views/serve.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Doc serving from Python. @@ -24,23 +25,25 @@ PYTHON_MEDIA (False) - Set this to True to serve docs & media from Python SERVE_DOCS (['private']) - The list of ['private', 'public'] docs to serve. """ -from __future__ import absolute_import +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + +import logging +import mimetypes +import os +from functools import wraps + from django.conf import settings -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.views.static import serve from readthedocs.builds.models import Version -from readthedocs.projects import constants -from readthedocs.projects.models import Project, ProjectRelationship from readthedocs.core.permissions import AdminPermission from readthedocs.core.resolver import resolve, resolve_path from readthedocs.core.symlink import PrivateSymlink, PublicSymlink - -import mimetypes -import os -import logging -from functools import wraps +from readthedocs.projects import constants +from readthedocs.projects.models import Project, ProjectRelationship log = logging.getLogger(__name__) @@ -53,8 +56,10 @@ def map_subproject_slug(view_func): .. warning:: Does not take into account any kind of privacy settings. """ + @wraps(view_func) - def inner_view(request, subproject=None, subproject_slug=None, *args, **kwargs): + def inner_view( + request, subproject=None, subproject_slug=None, *args, **kwargs): if subproject is None and subproject_slug: try: subproject = Project.objects.get(slug=subproject_slug) @@ -69,6 +74,7 @@ def map_subproject_slug(view_func): except (ProjectRelationship.DoesNotExist, KeyError): raise Http404 return view_func(request, subproject=subproject, *args, **kwargs) + return inner_view @@ -80,6 +86,7 @@ def map_project_slug(view_func): .. warning:: Does not take into account any kind of privacy settings. """ + @wraps(view_func) def inner_view(request, project=None, project_slug=None, *args, **kwargs): if project is None: @@ -90,13 +97,14 @@ def map_project_slug(view_func): except Project.DoesNotExist: raise Http404('Project does not exist.') return view_func(request, project=project, *args, **kwargs) + return inner_view @map_project_slug @map_subproject_slug def redirect_project_slug(request, project, subproject): # pylint: disable=unused-argument - """Handle / -> /en/latest/ directs on subdomains""" + """Handle / -> /en/latest/ directs on subdomains.""" return HttpResponseRedirect(resolve(subproject or project)) @@ -104,7 +112,8 @@ def redirect_project_slug(request, project, subproject): # pylint: disable=unus @map_subproject_slug def redirect_page_with_filename(request, project, subproject, filename): # pylint: disable=unused-argument # noqa """Redirect /page/file.html to /en/latest/file.html.""" - return HttpResponseRedirect(resolve(subproject or project, filename=filename)) + return HttpResponseRedirect( + resolve(subproject or project, filename=filename)) def _serve_401(request, project): @@ -121,15 +130,16 @@ def _serve_file(request, filename, basepath): return serve(request, filename, basepath) else: # Serve from Nginx - content_type, encoding = mimetypes.guess_type(os.path.join(basepath, filename)) + content_type, encoding = mimetypes.guess_type( + os.path.join(basepath, filename)) content_type = content_type or 'application/octet-stream' response = HttpResponse(content_type=content_type) if encoding: - response["Content-Encoding"] = encoding + response['Content-Encoding'] = encoding try: response['X-Accel-Redirect'] = os.path.join( basepath[len(settings.SITE_ROOT):], - filename + filename, ) except UnicodeEncodeError: raise Http404 @@ -139,9 +149,11 @@ def _serve_file(request, filename, basepath): @map_project_slug @map_subproject_slug -def serve_docs(request, project, subproject, - lang_slug=None, version_slug=None, filename=''): - """Exists to map existing proj, lang, version, filename views to the file format.""" +def serve_docs( + request, project, subproject, lang_slug=None, version_slug=None, + filename=''): + """Exists to map existing proj, lang, version, filename views to the file + format.""" if not version_slug: version_slug = project.get_default_version() try: @@ -153,18 +165,20 @@ def serve_docs(request, project, subproject, raise Http404('Version does not exist.') filename = resolve_path( subproject or project, # Resolve the subproject if it exists - version_slug=version_slug, language=lang_slug, filename=filename, + version_slug=version_slug, + language=lang_slug, + filename=filename, subdomain=True, # subdomain will make it a "full" path without a URL prefix ) - if ( - version.privacy_level == constants.PRIVATE and - not AdminPermission.is_member(user=request.user, obj=project) - ): + if (version.privacy_level == constants.PRIVATE and + not AdminPermission.is_member(user=request.user, obj=project)): return _serve_401(request, project) - return _serve_symlink_docs(request, - filename=filename, - project=project, - privacy_level=version.privacy_level) + return _serve_symlink_docs( + request, + filename=filename, + project=project, + privacy_level=version.privacy_level, + ) @map_project_slug @@ -184,7 +198,7 @@ def _serve_symlink_docs(request, project, privacy_level, filename=''): serve_docs = getattr(settings, 'SERVE_DOCS', [constants.PRIVATE]) - if (settings.DEBUG or constants.PUBLIC in serve_docs) and privacy_level != constants.PRIVATE: + if (settings.DEBUG or constants.PUBLIC in serve_docs) and privacy_level != constants.PRIVATE: # yapf: disable public_symlink = PublicSymlink(project) basepath = public_symlink.project_root if os.path.exists(os.path.join(basepath, filename)): @@ -192,7 +206,7 @@ def _serve_symlink_docs(request, project, privacy_level, filename=''): else: files_tried.append(os.path.join(basepath, filename)) - if (settings.DEBUG or constants.PRIVATE in serve_docs) and privacy_level == constants.PRIVATE: + if (settings.DEBUG or constants.PRIVATE in serve_docs) and privacy_level == constants.PRIVATE: # yapf: disable # Handle private private_symlink = PrivateSymlink(project) @@ -203,4 +217,5 @@ def _serve_symlink_docs(request, project, privacy_level, filename=''): else: files_tried.append(os.path.join(basepath, filename)) - raise Http404('File not found. Tried these files: %s' % ','.join(files_tried)) + raise Http404( + 'File not found. Tried these files: %s' % ','.join(files_tried)) diff --git a/readthedocs/gold/views.py b/readthedocs/gold/views.py index 76ce1d5a9..29826ce70 100644 --- a/readthedocs/gold/views.py +++ b/readthedocs/gold/views.py @@ -1,25 +1,29 @@ +# -*- coding: utf-8 -*- """Gold subscription views.""" -from __future__ import absolute_import -from django.core.urlresolvers import reverse, reverse_lazy +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + from django.conf import settings -from django.contrib.messages.views import SuccessMessageMixin from django.contrib import messages -from django.http import HttpResponseRedirect -from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required +from django.contrib.messages.views import SuccessMessageMixin +from django.core.urlresolvers import reverse, reverse_lazy +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, render from django.utils.translation import ugettext_lazy as _ -from vanilla import DeleteView, UpdateView, DetailView +from vanilla import DeleteView, DetailView, UpdateView from readthedocs.core.mixins import LoginRequiredMixin -from readthedocs.projects.models import Project, Domain from readthedocs.payments.mixins import StripeMixin +from readthedocs.projects.models import Domain, Project -from .forms import GoldSubscriptionForm, GoldProjectForm +from .forms import GoldProjectForm, GoldSubscriptionForm from .models import GoldUser -class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin): +class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, + LoginRequiredMixin): """Gold subscription mixin for view classes.""" @@ -41,8 +45,7 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin return reverse_lazy('gold_detail') def get_template_names(self): - return ('gold/subscription{0}.html' - .format(self.template_name_suffix)) + return ('gold/subscription{0}.html'.format(self.template_name_suffix)) def get_context_data(self, **kwargs): context = super(GoldSubscriptionMixin, self).get_context_data(**kwargs) @@ -50,6 +53,7 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin context['domains'] = domains return context + # Subscription Views @@ -99,7 +103,8 @@ def projects(request): gold_projects = gold_user.projects.all() if request.method == 'POST': - form = GoldProjectForm(data=request.POST, user=gold_user, projects=gold_projects) + form = GoldProjectForm( + data=request.POST, user=gold_user, projects=gold_projects) if form.is_valid(): to_add = Project.objects.get(slug=form.cleaned_data['project']) gold_user.projects.add(to_add) @@ -107,15 +112,14 @@ def projects(request): else: form = GoldProjectForm() - return render(request, - 'gold/projects.html', - { - 'form': form, - 'gold_user': gold_user, - 'publishable': settings.STRIPE_PUBLISHABLE, - 'user': request.user, - 'projects': gold_projects - }) + return render( + request, 'gold/projects.html', { + 'form': form, + 'gold_user': gold_user, + 'publishable': settings.STRIPE_PUBLISHABLE, + 'user': request.user, + 'projects': gold_projects, + }) @login_required diff --git a/readthedocs/profiles/views.py b/readthedocs/profiles/views.py index b99de5ba1..09abd1579 100644 --- a/readthedocs/profiles/views.py +++ b/readthedocs/profiles/views.py @@ -1,6 +1,8 @@ +# -*- coding: utf-8 -*- """Views for creating, editing and viewing site-specific user profiles.""" -from __future__ import absolute_import +from __future__ import ( + absolute_import, division, print_function, unicode_literals) from django.contrib import messages from django.contrib.auth import logout @@ -15,9 +17,10 @@ from django.template.context import RequestContext from readthedocs.core.forms import UserDeleteForm -def create_profile(request, form_class, success_url=None, - template_name='profiles/private/create_profile.html', - extra_context=None): +def create_profile( + request, form_class, success_url=None, + template_name='profiles/private/create_profile.html', + extra_context=None): """ Create a profile for the current user, if one doesn't already exist. @@ -63,7 +66,6 @@ def create_profile(request, form_class, success_url=None, ``template_name`` keyword argument, or :template:`profiles/create_profile.html`. - """ try: profile_obj = request.user.profile @@ -81,8 +83,9 @@ def create_profile(request, form_class, success_url=None, # if success_url is None: - success_url = reverse('profiles_profile_detail', - kwargs={'username': request.user.username}) + success_url = reverse( + 'profiles_profile_detail', + kwargs={'username': request.user.username}) if request.method == 'POST': form = form_class(data=request.POST, files=request.FILES) if form.is_valid(): @@ -103,12 +106,14 @@ def create_profile(request, form_class, success_url=None, context.update({'form': form}) return render(request, template_name, context=context) + + create_profile = login_required(create_profile) -def edit_profile(request, form_class, success_url=None, - template_name='profiles/private/edit_profile.html', - extra_context=None): +def edit_profile( + request, form_class, success_url=None, + template_name='profiles/private/edit_profile.html', extra_context=None): """ Edit the current user's profile. @@ -153,7 +158,6 @@ def edit_profile(request, form_class, success_url=None, ``template_name`` keyword argument or :template:`profiles/edit_profile.html`. - """ try: profile_obj = request.user.profile @@ -161,10 +165,12 @@ def edit_profile(request, form_class, success_url=None, return HttpResponseRedirect(reverse('profiles_profile_create')) if success_url is None: - success_url = reverse('profiles_profile_detail', - kwargs={'username': request.user.username}) + success_url = reverse( + 'profiles_profile_detail', + kwargs={'username': request.user.username}) if request.method == 'POST': - form = form_class(data=request.POST, files=request.FILES, instance=profile_obj) + form = form_class( + data=request.POST, files=request.FILES, instance=profile_obj) if form.is_valid(): form.save() return HttpResponseRedirect(success_url) @@ -183,6 +189,8 @@ def edit_profile(request, form_class, success_url=None, 'user': profile_obj.user, }) return render(request, template_name, context=context) + + edit_profile = login_required(edit_profile) @@ -205,9 +213,10 @@ def delete_account(request): return render(request, template_name, {'form': form}) -def profile_detail(request, username, public_profile_field=None, - template_name='profiles/public/profile_detail.html', - extra_context=None): +def profile_detail( + request, username, public_profile_field=None, + template_name='profiles/public/profile_detail.html', + extra_context=None): """ Detail view of a user's profile. @@ -252,7 +261,6 @@ def profile_detail(request, username, public_profile_field=None, ``template_name`` keyword argument or :template:`profiles/profile_detail.html`. - """ user = get_object_or_404(User, username=username) try: diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 23290dda2..a9c712139 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -1,46 +1,44 @@ -"""Project views for authenticated users.""" +"""Project views for authenticated users""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals) -from __future__ import absolute_import import logging +from allauth.socialaccount.models import SocialAccount +from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.contrib import messages from django.core.urlresolvers import reverse -from django.http import (HttpResponseRedirect, HttpResponseNotAllowed, - Http404, HttpResponseBadRequest) -from django.shortcuts import get_object_or_404, render -from django.views.generic import View, TemplateView, ListView -from django.utils.translation import ugettext_lazy as _ -from django.utils.safestring import mark_safe +from django.http import ( + Http404, HttpResponseBadRequest, HttpResponseNotAllowed, + HttpResponseRedirect) from django.middleware.csrf import get_token +from django.shortcuts import get_object_or_404, render +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import ListView, TemplateView, View from formtools.wizard.views import SessionWizardView -from allauth.socialaccount.models import SocialAccount - -from vanilla import CreateView, DeleteView, UpdateView, DetailView, GenericView +from vanilla import CreateView, DeleteView, DetailView, GenericView, UpdateView from readthedocs.bookmarks.models import Bookmark -from readthedocs.builds.models import Version from readthedocs.builds.forms import AliasForm, VersionForm -from readthedocs.builds.models import VersionAlias -from readthedocs.core.utils import trigger_build, broadcast -from readthedocs.core.mixins import ListViewWithForm +from readthedocs.builds.models import Version, VersionAlias +from readthedocs.core.mixins import ListViewWithForm, LoginRequiredMixin +from readthedocs.core.utils import broadcast, trigger_build from readthedocs.integrations.models import HttpExchange, Integration -from readthedocs.projects.forms import ( - ProjectBasicsForm, ProjectExtraForm, ProjectAdvancedForm, - UpdateProjectForm, ProjectRelationshipForm, - build_versions_form, UserForm, EmailHookForm, TranslationForm, - RedirectForm, WebHookForm, DomainForm, IntegrationForm, - ProjectAdvertisingForm) -from readthedocs.projects.models import ( - Project, ProjectRelationship, EmailHook, WebHook, Domain) -from readthedocs.projects.views.base import ProjectAdminMixin, ProjectSpamMixin -from readthedocs.projects import tasks from readthedocs.oauth.services import registry from readthedocs.oauth.utils import attach_webhook, update_webhook - -from readthedocs.core.mixins import LoginRequiredMixin +from readthedocs.projects import tasks +from readthedocs.projects.forms import ( + DomainForm, EmailHookForm, IntegrationForm, ProjectAdvancedForm, + ProjectAdvertisingForm, ProjectBasicsForm, ProjectExtraForm, + ProjectRelationshipForm, RedirectForm, TranslationForm, UpdateProjectForm, + UserForm, WebHookForm, build_versions_form) +from readthedocs.projects.models import ( + Domain, EmailHook, Project, ProjectRelationship, WebHook) from readthedocs.projects.signals import project_import +from readthedocs.projects.views.base import ProjectAdminMixin, ProjectSpamMixin log = logging.getLogger(__name__) @@ -82,18 +80,18 @@ def project_manage(__, project_slug): Now redirects to the normal /projects/ view. """ - return HttpResponseRedirect(reverse('projects_detail', - args=[project_slug])) + return HttpResponseRedirect(reverse('projects_detail', args=[project_slug])) @login_required def project_comments_moderation(request, project_slug): - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) return render( request, 'projects/project_comments_moderation.html', - {'project': project}) + {'project': project}, + ) class ProjectUpdate(ProjectSpamMixin, PrivateViewMixin, UpdateView): @@ -137,8 +135,8 @@ def project_versions(request, project_slug): Shows the available versions and lets the user choose which ones he would like to have built. """ - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) if not project.is_imported: raise Http404 @@ -153,17 +151,19 @@ def project_versions(request, project_slug): project_dashboard = reverse('projects_detail', args=[project.slug]) return HttpResponseRedirect(project_dashboard) - return render(request, - 'projects/project_versions.html', - {'form': form, 'project': project}) + return render( + request, 'projects/project_versions.html', + {'form': form, 'project': project}) @login_required def project_version_detail(request, project_slug, version_slug): """Project version detail page.""" - project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) version = get_object_or_404( - Version.objects.public(user=request.user, project=project, only_active=False), + Version.objects.public( + user=request.user, project=project, only_active=False), slug=version_slug) form = VersionForm(request.POST or None, instance=version) @@ -173,14 +173,15 @@ def project_version_detail(request, project_slug, version_slug): if form.has_changed(): if 'active' in form.changed_data and version.active is False: log.info('Removing files for version %s', version.slug) - broadcast(type='app', task=tasks.clear_artifacts, args=[version.pk]) + broadcast( + type='app', task=tasks.clear_artifacts, args=[version.pk]) version.built = False version.save() url = reverse('project_version_list', args=[project.slug]) return HttpResponseRedirect(url) - return render(request, - 'projects/project_version_detail.html', + return render( + request, 'projects/project_version_detail.html', {'form': form, 'project': project, 'version': version}) @@ -192,8 +193,8 @@ def project_delete(request, project_slug): Make a project as deleted on POST, otherwise show a form asking for confirmation of delete. """ - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) if request.method == 'POST': broadcast(type='app', task=tasks.remove_dir, args=[project.doc_path]) @@ -202,17 +203,14 @@ def project_delete(request, project_slug): project_dashboard = reverse('projects_dashboard') return HttpResponseRedirect(project_dashboard) - return render(request, - 'projects/project_delete.html', - {'project': project}) + return render(request, 'projects/project_delete.html', {'project': project}) class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView): """Project import wizard.""" - form_list = [('basics', ProjectBasicsForm), - ('extra', ProjectExtraForm)] + form_list = [('basics', ProjectBasicsForm), ('extra', ProjectExtraForm),] condition_dict = {'extra': lambda self: self.is_advanced()} def get_form_kwargs(self, step=None): @@ -253,8 +251,8 @@ class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView): project.save() project_import.send(sender=project, request=self.request) trigger_build(project, basic=basic_only) - return HttpResponseRedirect(reverse('projects_detail', - args=[project.slug])) + return HttpResponseRedirect( + reverse('projects_detail', args=[project.slug])) def is_advanced(self): """Determine if the user selected the `show advanced` field.""" @@ -278,8 +276,8 @@ class ImportDemoView(PrivateViewMixin, View): self.kwargs = kwargs data = self.get_form_data() - project = (Project.objects.for_admin_user(request.user) - .filter(repo=data['repo']).first()) + project = Project.objects.for_admin_user( + request.user).filter(repo=data['repo']).first() if project is not None: messages.success( request, _('The demo project is already imported!')) @@ -295,11 +293,13 @@ class ImportDemoView(PrivateViewMixin, View): else: for (__, msg) in list(form.errors.items()): log.error(msg) - messages.error(request, - _('There was a problem adding the demo project')) + messages.error( + request, + _('There was a problem adding the demo project'), + ) return HttpResponseRedirect(reverse('projects_dashboard')) - return HttpResponseRedirect(reverse('projects_detail', - args=[project.slug])) + return HttpResponseRedirect( + reverse('projects_detail', args=[project.slug])) def get_form_data(self): """Get form data to post to import form.""" @@ -337,19 +337,23 @@ class ImportView(PrivateViewMixin, TemplateView): deprecated_accounts = ( SocialAccount.objects .filter(user=self.request.user) - .exclude(provider__in=[service.adapter.provider_id - for service in registry]) - ) + .exclude( + provider__in=[ + service.adapter.provider_id for service in registry + ]) + ) # yapf: disable for account in deprecated_accounts: provider_account = account.get_provider_account() messages.error( request, - mark_safe( - (_('There is a problem with your {service} account, ' - 'try reconnecting your account on your ' - 'connected services page.') - .format(service=provider_account.get_brand()['name'], - url=reverse('socialaccount_connections')))) + mark_safe(( + _( + 'There is a problem with your {service} account, ' + 'try reconnecting your account on your ' + 'connected services page.').format( + service=provider_account.get_brand()['name'], + url=reverse('socialaccount_connections')) + )) # yapf: disable ) return super(ImportView, self).get(request, *args, **kwargs) @@ -367,17 +371,16 @@ class ImportView(PrivateViewMixin, TemplateView): def get_context_data(self, **kwargs): context = super(ImportView, self).get_context_data(**kwargs) context['view_csrf_token'] = get_token(self.request) - context['has_connected_accounts'] = (SocialAccount - .objects - .filter(user=self.request.user) - .exists()) + context['has_connected_accounts'] = SocialAccount.objects.filter( + user=self.request.user).exists() return context @login_required def edit_alias(request, project_slug, alias_id=None): """Edit project alias form view.""" - proj = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) + proj = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) if alias_id: alias = proj.aliases.get(pk=alias_id) form = AliasForm(instance=alias, data=request.POST or None) @@ -387,9 +390,11 @@ def edit_alias(request, project_slug, alias_id=None): if request.method == 'POST' and form.is_valid(): alias = form.save() return HttpResponseRedirect(alias.project.get_absolute_url()) - return render(request, + return render( + request, 'projects/alias_edit.html', - {'form': form}) + {'form': form}, + ) class AliasList(PrivateViewMixin, ListView): @@ -417,11 +422,15 @@ class ProjectRelationshipMixin(ProjectAdminMixin, PrivateViewMixin): def get_form(self, data=None, files=None, **kwargs): kwargs['user'] = self.request.user - return super(ProjectRelationshipMixin, self).get_form(data, files, **kwargs) + return super(ProjectRelationshipMixin, + self).get_form(data, files, **kwargs) def form_valid(self, form): - broadcast(type='app', task=tasks.symlink_subproject, - args=[self.get_project().pk]) + broadcast( + type='app', + task=tasks.symlink_subproject, + args=[self.get_project().pk], + ) return super(ProjectRelationshipMixin, self).form_valid(form) def get_success_url(self): @@ -453,8 +462,8 @@ class ProjectRelationshipDelete(ProjectRelationshipMixin, DeleteView): @login_required def project_users(request, project_slug): """Project users view and form view.""" - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) form = UserForm(data=request.POST or None, project=project) @@ -465,17 +474,21 @@ def project_users(request, project_slug): users = project.users.all() - return render(request, + return render( + request, 'projects/project_users.html', - {'form': form, 'project': project, 'users': users}) + {'form': form, 'project': project, 'users': users}, + ) @login_required def project_users_delete(request, project_slug): if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') - project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) - user = get_object_or_404(User.objects.all(), username=request.POST.get('username')) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) + user = get_object_or_404( + User.objects.all(), username=request.POST.get('username')) if user == request.user: raise Http404 project.users.remove(user) @@ -486,8 +499,8 @@ def project_users_delete(request, project_slug): @login_required def project_notifications(request, project_slug): """Project notification view and form view.""" - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) email_form = EmailHookForm(data=request.POST or None, project=project) webhook_form = WebHookForm(data=request.POST or None, project=project) @@ -497,14 +510,17 @@ def project_notifications(request, project_slug): email_form.save() if webhook_form.is_valid(): webhook_form.save() - project_dashboard = reverse('projects_notifications', - args=[project.slug]) + project_dashboard = reverse( + 'projects_notifications', + args=[project.slug], + ) return HttpResponseRedirect(project_dashboard) emails = project.emailhook_notifications.all() urls = project.webhook_notifications.all() - return render(request, + return render( + request, 'projects/project_notifications.html', { 'email_form': email_form, @@ -512,19 +528,22 @@ def project_notifications(request, project_slug): 'project': project, 'emails': emails, 'urls': urls, - }) + }, + ) @login_required def project_comments_settings(request, project_slug): - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) - return render(request, + return render( + request, 'projects/project_comments_settings.html', { 'project': project, - }) + }, + ) @login_required @@ -532,13 +551,15 @@ def project_notifications_delete(request, project_slug): """Project notifications delete confirmation view.""" if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) try: - project.emailhook_notifications.get(email=request.POST.get('email')).delete() + project.emailhook_notifications.get( + email=request.POST.get('email')).delete() except EmailHook.DoesNotExist: try: - project.webhook_notifications.get(url=request.POST.get('email')).delete() + project.webhook_notifications.get( + url=request.POST.get('email')).delete() except WebHook.DoesNotExist: raise Http404 project_dashboard = reverse('projects_notifications', args=[project.slug]) @@ -548,27 +569,41 @@ def project_notifications_delete(request, project_slug): @login_required def project_translations(request, project_slug): """Project translations view and form view.""" - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) form = TranslationForm(data=request.POST or None, parent=project) if request.method == 'POST' and form.is_valid(): form.save() - project_dashboard = reverse('projects_translations', - args=[project.slug]) + project_dashboard = reverse( + 'projects_translations', + args=[project.slug], + ) return HttpResponseRedirect(project_dashboard) lang_projects = project.translations.all() - return render(request, + return render( + request, 'projects/project_translations.html', - {'form': form, 'project': project, 'lang_projects': lang_projects}) + { + 'form': form, + 'project': project, + 'lang_projects': lang_projects, + }, + ) @login_required def project_translations_delete(request, project_slug, child_slug): - project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) - subproj = get_object_or_404(Project.objects.for_admin_user(request.user), slug=child_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), + slug=project_slug, + ) + subproj = get_object_or_404( + Project.objects.for_admin_user(request.user), + slug=child_slug, + ) project.translations.remove(subproj) project_dashboard = reverse('projects_translations', args=[project.slug]) return HttpResponseRedirect(project_dashboard) @@ -577,8 +612,8 @@ def project_translations_delete(request, project_slug, child_slug): @login_required def project_redirects(request, project_slug): """Project redirects view and form view.""" - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) form = RedirectForm(data=request.POST or None, project=project) @@ -589,8 +624,8 @@ def project_redirects(request, project_slug): redirects = project.redirects.all() - return render(request, - 'projects/project_redirects.html', + return render( + request, 'projects/project_redirects.html', {'form': form, 'project': project, 'redirects': redirects}) @@ -599,16 +634,16 @@ def project_redirects_delete(request, project_slug): """Project redirect delete view.""" if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') - project = get_object_or_404(Project.objects.for_admin_user(request.user), - slug=project_slug) - redirect = get_object_or_404(project.redirects, - pk=request.POST.get('id_pk')) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) + redirect = get_object_or_404( + project.redirects, pk=request.POST.get('id_pk')) if redirect.project == project: redirect.delete() else: raise Http404 - return HttpResponseRedirect(reverse('projects_redirects', - args=[project.slug])) + return HttpResponseRedirect( + reverse('projects_redirects', args=[project.slug])) @login_required @@ -618,9 +653,11 @@ def project_version_delete_html(request, project_slug, version_slug): This marks a version as not built """ - project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) + project = get_object_or_404( + Project.objects.for_admin_user(request.user), slug=project_slug) version = get_object_or_404( - Version.objects.public(user=request.user, project=project, only_active=False), + Version.objects.public( + user=request.user, project=project, only_active=False), slug=version_slug) if not version.active: @@ -628,7 +665,8 @@ def project_version_delete_html(request, project_slug, version_slug): version.save() broadcast(type='app', task=tasks.clear_artifacts, args=[version.pk]) else: - return HttpResponseBadRequest("Can't delete HTML for an active version.") + return HttpResponseBadRequest( + "Can't delete HTML for an active version.") return HttpResponseRedirect( reverse('project_version_list', kwargs={'project_slug': project_slug})) @@ -707,7 +745,7 @@ class IntegrationCreate(IntegrationMixin, CreateView): kwargs={ 'project_slug': self.get_project().slug, 'integration_pk': self.object.id, - } + }, ) @@ -726,8 +764,9 @@ class IntegrationDetail(IntegrationMixin, DetailView): return self.template_name integration_type = self.get_integration().integration_type suffix = self.SUFFIX_MAP.get(integration_type, integration_type) - return ('projects/integration_{0}{1}.html' - .format(suffix, self.template_name_suffix)) + return ( + 'projects/integration_{0}{1}.html' + .format(suffix, self.template_name_suffix)) class IntegrationDelete(IntegrationMixin, DeleteView): @@ -743,9 +782,7 @@ class IntegrationExchangeDetail(IntegrationMixin, DetailView): template_name = 'projects/integration_exchange_detail.html' def get_queryset(self): - return self.model.objects.filter( - integrations=self.get_integration() - ) + return self.model.objects.filter(integrations=self.get_integration()) def get_object(self): return DetailView.get_object(self) diff --git a/readthedocs/projects/views/public.py b/readthedocs/projects/views/public.py index 85a0aac76..39adc01dd 100644 --- a/readthedocs/projects/views/public.py +++ b/readthedocs/projects/views/public.py @@ -1,38 +1,40 @@ -"""Public project views.""" +"""Public project views""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals) -from __future__ import absolute_import -from collections import OrderedDict -import operator -import os import json import logging import mimetypes +import operator +import os +from collections import OrderedDict -from django.core.urlresolvers import reverse -from django.core.cache import cache +import requests from django.conf import settings from django.contrib import messages from django.contrib.auth.models import User from django.contrib.staticfiles.templatetags.staticfiles import static -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.core.cache import cache +from django.core.urlresolvers import reverse +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.views.decorators.cache import never_cache -from django.views.generic import ListView, DetailView - +from django.views.generic import DetailView, ListView from taggit.models import Tag -import requests -from .base import ProjectOnboardMixin from readthedocs.builds.constants import LATEST from readthedocs.builds.models import Version from readthedocs.builds.views import BuildTriggerMixin -from readthedocs.projects.models import Project, ImportedFile +from readthedocs.projects.models import ImportedFile, Project from readthedocs.search.indexes import PageIndex from readthedocs.search.views import LOG_TEMPLATE +from .base import ProjectOnboardMixin + log = logging.getLogger(__name__) search_log = logging.getLogger(__name__ + '.search') -mimetypes.add_type("application/epub+zip", ".epub") +mimetypes.add_type('application/epub+zip', '.epub') class ProjectIndex(ListView): @@ -51,7 +53,8 @@ class ProjectIndex(ListView): self.tag = None if self.kwargs.get('username'): - self.user = get_object_or_404(User, username=self.kwargs.get('username')) + self.user = get_object_or_404( + User, username=self.kwargs.get('username')) queryset = queryset.filter(user=self.user) else: self.user = None @@ -64,6 +67,7 @@ class ProjectIndex(ListView): context['tag'] = self.tag return context + project_index = ProjectIndex.as_view() @@ -90,15 +94,16 @@ class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView): version_slug = project.get_default_version() - context['badge_url'] = "%s://%s%s?version=%s" % ( + context['badge_url'] = '%s://%s%s?version=%s' % ( protocol, settings.PRODUCTION_DOMAIN, reverse('project_badge', args=[project.slug]), project.get_default_version(), ) - context['site_url'] = "{url}?badge={version}".format( + context['site_url'] = '{url}?badge={version}'.format( url=project.get_docs_url(version_slug), - version=version_slug) + version=version_slug, + ) return context @@ -106,29 +111,31 @@ class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView): @never_cache def project_badge(request, project_slug): """Return a sweet badge for the project.""" - badge_path = "projects/badges/%s.svg" + badge_path = 'projects/badges/%s.svg' version_slug = request.GET.get('version', LATEST) try: version = Version.objects.public(request.user).get( project__slug=project_slug, slug=version_slug) except Version.DoesNotExist: - url = static(badge_path % "unknown") + url = static(badge_path % 'unknown') return HttpResponseRedirect(url) - version_builds = version.builds.filter(type='html', state='finished').order_by('-date') + version_builds = version.builds.filter(type='html', + state='finished').order_by('-date') if not version_builds.exists(): - url = static(badge_path % "unknown") + url = static(badge_path % 'unknown') return HttpResponseRedirect(url) last_build = version_builds[0] if last_build.success: - url = static(badge_path % "passing") + url = static(badge_path % 'passing') else: - url = static(badge_path % "failing") + url = static(badge_path % 'failing') return HttpResponseRedirect(url) def project_downloads(request, project_slug): """A detail view for a project with various dataz.""" - project = get_object_or_404(Project.objects.protected(request.user), slug=project_slug) + project = get_object_or_404( + Project.objects.protected(request.user), slug=project_slug) versions = Version.objects.public(user=request.user, project=project) version_data = OrderedDict() for version in versions: @@ -137,13 +144,15 @@ def project_downloads(request, project_slug): if data: version_data[version] = data - return render(request, + return render( + request, 'projects/project_downloads.html', { 'project': project, 'version_data': version_data, 'versions': versions, - }) + }, + ) def project_download_media(request, project_slug, type_, version_slug): @@ -163,23 +172,25 @@ def project_download_media(request, project_slug, type_, version_slug): ) privacy_level = getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') if privacy_level == 'public' or settings.DEBUG: - path = os.path.join(settings.MEDIA_URL, type_, project_slug, version_slug, - '%s.%s' % (project_slug, type_.replace('htmlzip', 'zip'))) + path = os.path.join( + settings.MEDIA_URL, type_, project_slug, version_slug, + '%s.%s' % (project_slug, type_.replace('htmlzip', 'zip'))) return HttpResponseRedirect(path) else: # Get relative media path - path = (version.project - .get_production_media_path( - type_=type_, version_slug=version_slug) - .replace(settings.PRODUCTION_ROOT, '/prod_artifacts')) + path = ( + version.project.get_production_media_path( + type_=type_, version_slug=version_slug) + .replace(settings.PRODUCTION_ROOT, '/prod_artifacts')) content_type, encoding = mimetypes.guess_type(path) content_type = content_type or 'application/octet-stream' response = HttpResponse(content_type=content_type) if encoding: - response["Content-Encoding"] = encoding + response['Content-Encoding'] = encoding response['X-Accel-Redirect'] = path # Include version in filename; this fixes a long-standing bug - filename = "%s-%s.%s" % (project_slug, version_slug, path.split('.')[-1]) + filename = '%s-%s.%s' % ( + project_slug, version_slug, path.split('.')[-1]) response['Content-Disposition'] = 'filename=%s' % filename return response @@ -190,7 +201,8 @@ def search_autocomplete(request): term = request.GET['term'] else: raise Http404 - queryset = (Project.objects.public(request.user).filter(name__icontains=term)[:20]) + queryset = Project.objects.public( + request.user).filter(name__icontains=term)[:20] ret_list = [] for project in queryset: @@ -231,12 +243,14 @@ def version_filter_autocomplete(request, project_slug): json_response = json.dumps(list(names)) return HttpResponse(json_response, content_type='text/javascript') elif resp_format == 'html': - return render(request, + return render( + request, 'core/version_list.html', { 'project': project, 'versions': versions, - }) + }, + ) return HttpResponse(status=400) @@ -246,7 +260,8 @@ def file_autocomplete(request, project_slug): term = request.GET['term'] else: raise Http404 - queryset = ImportedFile.objects.filter(project__slug=project_slug, path__icontains=term)[:20] + queryset = ImportedFile.objects.filter( + project__slug=project_slug, path__icontains=term)[:20] ret_list = [] for filename in queryset: @@ -269,43 +284,44 @@ def elastic_project_search(request, project_slug): user = '' if request.user.is_authenticated(): user = request.user - log.info(LOG_TEMPLATE.format( - user=user, - project=project or '', - type='inproject', - version=version_slug or '', - language='', - msg=query or '', - )) + log.info( + LOG_TEMPLATE.format( + user=user, + project=project or '', + type='inproject', + version=version_slug or '', + language='', + msg=query or '', + )) if query: kwargs = {} body = { - "query": { - "bool": { - "should": [ - {"match": {"title": {"query": query, "boost": 10}}}, - {"match": {"headers": {"query": query, "boost": 5}}}, - {"match": {"content": {"query": query}}}, + 'query': { + 'bool': { + 'should': [ + {'match': {'title': {'query': query, 'boost': 10}}}, + {'match': {'headers': {'query': query, 'boost': 5}}}, + {'match': {'content': {'query': query}}}, ] } }, - "highlight": { - "fields": { - "title": {}, - "headers": {}, - "content": {}, + 'highlight': { + 'fields': { + 'title': {}, + 'headers': {}, + 'content': {}, } }, - "fields": ["title", "project", "version", "path"], - "filter": { - "and": [ - {"term": {"project": project_slug}}, - {"term": {"version": version_slug}}, + 'fields': ['title', 'project', 'version', 'path'], + 'filter': { + 'and': [ + {'term': {'project': project_slug}}, + {'term': {'version': version_slug}}, ] }, - "size": 50 # TODO: Support pagination. + 'size': 50, # TODO: Support pagination. } # Add routing to optimize search by hitting the right shard. @@ -322,13 +338,15 @@ def elastic_project_search(request, project_slug): if isinstance(val, list): results['hits']['hits'][num]['fields'][key] = val[0] - return render(request, + return render( + request, 'search/elastic_project_search.html', { 'project': project, 'query': query, 'results': results, - }) + }, + ) def project_versions(request, project_slug): @@ -337,10 +355,11 @@ def project_versions(request, project_slug): Shows the available versions and lets the user choose which ones to build. """ - project = get_object_or_404(Project.objects.protected(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.protected(request.user), slug=project_slug) - versions = Version.objects.public(user=request.user, project=project, only_active=False) + versions = Version.objects.public( + user=request.user, project=project, only_active=False) active_versions = versions.filter(active=True) inactive_versions = versions.filter(active=False) @@ -352,38 +371,46 @@ def project_versions(request, project_slug): if wiped and wiped_version.count(): messages.success(request, 'Version wiped: ' + wiped) - return render(request, + return render( + request, 'projects/project_version_list.html', { 'inactive_versions': inactive_versions, 'active_versions': active_versions, 'project': project, - }) + }, + ) def project_analytics(request, project_slug): """Have a analytics API placeholder.""" - project = get_object_or_404(Project.objects.protected(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.protected(request.user), slug=project_slug) analytics_cache = cache.get('analytics:%s' % project_slug) if analytics_cache: analytics = json.loads(analytics_cache) else: try: resp = requests.get( - '{host}/api/v1/index/1/heatmap/'.format(host=settings.GROK_API_HOST), - params={'project': project.slug, 'days': 7, 'compare': True} - ) + '{host}/api/v1/index/1/heatmap/'.format( + host=settings.GROK_API_HOST), + params={'project': project.slug, 'days': 7, 'compare': True}) analytics = resp.json() cache.set('analytics:%s' % project_slug, resp.content, 1800) except requests.exceptions.RequestException: analytics = None if analytics: - page_list = list(reversed(sorted(list(analytics['page'].items()), - key=operator.itemgetter(1)))) - version_list = list(reversed(sorted(list(analytics['version'].items()), - key=operator.itemgetter(1)))) + page_list = list( + reversed( + sorted( + list(analytics['page'].items()), + key=operator.itemgetter(1)))) + version_list = list( + reversed( + sorted( + list(analytics['version'].items()), + key=operator.itemgetter(1)))) else: page_list = [] version_list = [] @@ -393,7 +420,8 @@ def project_analytics(request, project_slug): page_list = page_list[:20] version_list = version_list[:20] - return render(request, + return render( + request, 'projects/project_analytics.html', { 'project': project, @@ -401,23 +429,26 @@ def project_analytics(request, project_slug): 'page_list': page_list, 'version_list': version_list, 'full': full, - }) + }, + ) def project_embed(request, project_slug): """Have a content API placeholder.""" - project = get_object_or_404(Project.objects.protected(request.user), - slug=project_slug) + project = get_object_or_404( + Project.objects.protected(request.user), slug=project_slug) version = project.versions.get(slug=LATEST) files = version.imported_files.filter(name__endswith='.html').order_by('path') - return render(request, + return render( + request, 'projects/project_embed.html', { 'project': project, 'files': files, 'settings': { 'PUBLIC_API_URL': settings.PUBLIC_API_URL, - 'URI': request.build_absolute_uri(location='/').rstrip('/') - } - }) + 'URI': request.build_absolute_uri(location='/').rstrip('/'), + }, + }, + ) diff --git a/readthedocs/search/views.py b/readthedocs/search/views.py index b014f0b12..7d3a51d5f 100644 --- a/readthedocs/search/views.py +++ b/readthedocs/search/views.py @@ -1,9 +1,11 @@ +# -*- coding: utf-8 -*- """Search views.""" -from __future__ import absolute_import -from __future__ import print_function -from pprint import pprint +from __future__ import ( + absolute_import, division, print_function, unicode_literals) + import collections import logging +from pprint import pprint from django.conf import settings from django.shortcuts import render @@ -11,26 +13,33 @@ from django.shortcuts import render from readthedocs.builds.constants import LATEST from readthedocs.search import lib as search_lib - log = logging.getLogger(__name__) -LOG_TEMPLATE = u"(Elastic Search) [{user}:{type}] [{project}:{version}:{language}] {msg}" - +LOG_TEMPLATE = u'(Elastic Search) [{user}:{type}] [{project}:{version}:{language}] {msg}' UserInput = collections.namedtuple( - 'UserInput', ('query', 'type', 'project', 'version', 'taxonomy', 'language')) + 'UserInput', + ( + 'query', + 'type', + 'project', + 'version', + 'taxonomy', + 'language', + ), +) def elastic_search(request): - """Use Elasticsearch for global search""" + """Use Elasticsearch for global search.""" user_input = UserInput( query=request.GET.get('q'), type=request.GET.get('type', 'project'), project=request.GET.get('project'), version=request.GET.get('version', LATEST), taxonomy=request.GET.get('taxonomy'), - language=request.GET.get('language') + language=request.GET.get('language'), ) - results = "" + results = '' facets = {} @@ -65,21 +74,23 @@ def elastic_search(request): user = '' if request.user.is_authenticated(): user = request.user - log.info(LOG_TEMPLATE.format( - user=user, - project=user_input.project or '', - type=user_input.type or '', - version=user_input.version or '', - language=user_input.language or '', - msg=user_input.query or '', - )) + log.info( + LOG_TEMPLATE.format( + user=user, + project=user_input.project or '', + type=user_input.type or '', + version=user_input.version or '', + language=user_input.language or '', + msg=user_input.query or '', + )) template_vars = user_input._asdict() template_vars.update({ 'results': results, 'facets': facets, }) - return render(request, + return render( + request, 'search/elastic_search.html', template_vars, )