A lot of styling/autolinting

more-gsoc
Manuel Kaufmann 2017-12-11 21:13:20 -05:00
parent bf5119f45a
commit dc96c6d4a7
10 changed files with 573 additions and 441 deletions

View File

@ -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<resource_name>%s)/schema/$" % self._meta.resource_name,
self.wrap_view('get_schema'), name="api_get_schema"),
url(r"^(?P<resource_name>%s)/search%s$" % (
self._meta.resource_name, trailing_slash()),
self.wrap_view('get_search'), name="api_get_search"),
url(r"^(?P<resource_name>%s)/(?P<pk>\d+)/sync_versions%s$" % (
self._meta.resource_name, trailing_slash()),
self.wrap_view('sync_versions'), name="api_sync_versions"),
url((r"^(?P<resource_name>%s)/(?P<slug>[a-z-_]+)/$")
% self._meta.resource_name, self.wrap_view('dispatch_detail'),
name="api_dispatch_detail"),
url(
r'^(?P<resource_name>%s)/schema/$' % self._meta.resource_name,
self.wrap_view('get_schema'), name='api_get_schema'),
url(
r'^(?P<resource_name>%s)/search%s$' %
(self._meta.resource_name, trailing_slash()),
self.wrap_view('get_search'), name='api_get_search'),
url(
r'^(?P<resource_name>%s)/(?P<pk>\d+)/sync_versions%s$' %
(self._meta.resource_name, trailing_slash()),
self.wrap_view('sync_versions'), name='api_sync_versions'),
url((r'^(?P<resource_name>%s)/(?P<slug>[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<resource_name>%s)/schema/$"
% self._meta.resource_name,
self.wrap_view('get_schema'),
name="api_get_schema"),
url(r"^(?P<resource_name>%s)/(?P<project__slug>[a-z-_]+[a-z0-9-_]+)/$" # noqa
url(
r'^(?P<resource_name>%s)/schema/$' % self._meta.resource_name,
self.wrap_view('get_schema'), name='api_get_schema'),
url(
r'^(?P<resource_name>%s)/(?P<project__slug>[a-z-_]+[a-z0-9-_]+)/$' # noqa
% self._meta.resource_name,
self.wrap_view('dispatch_list'),
name="api_version_list"),
url((r"^(?P<resource_name>%s)/(?P<project_slug>[a-z-_]+[a-z0-9-_]+)/(?P"
r"<version_slug>[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<resource_name>%s)/(?P<project_slug>[a-z-_]+[a-z0-9-_]+)/(?P'
r'<version_slug>[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<resource_name>%s)/schema/$" %
self._meta.resource_name,
self.wrap_view('get_schema'),
name="api_get_schema"),
url(r"^(?P<resource_name>%s)/search%s$" %
url(
r'^(?P<resource_name>%s)/schema/$' % self._meta.resource_name,
self.wrap_view('get_schema'), name='api_get_schema'),
url(
r'^(?P<resource_name>%s)/search%s$' %
(self._meta.resource_name, trailing_slash()),
self.wrap_view('get_search'),
name="api_get_search"),
url(r"^(?P<resource_name>%s)/anchor%s$" %
self.wrap_view('get_search'), name='api_get_search'),
url(
r'^(?P<resource_name>%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<resource_name>%s)/schema/$" %
self._meta.resource_name,
self.wrap_view('get_schema'),
name="api_get_schema"),
url(r"^(?P<resource_name>%s)/(?P<username>[a-z-_]+)/$" %
self._meta.resource_name,
self.wrap_view('dispatch_detail'),
name="api_dispatch_detail"),
url(
r'^(?P<resource_name>%s)/schema/$' % self._meta.resource_name,
self.wrap_view('get_schema'), name='api_get_schema'),
url(
r'^(?P<resource_name>%s)/(?P<username>[a-z-_]+)/$' %
self._meta.resource_name, self.wrap_view('dispatch_detail'),
name='api_dispatch_detail'),
]

View File

@ -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',
)

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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:

View File

@ -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/<slug> 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 '
'<a href="{url}">connected services page</a>.')
.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 '
'<a href="{url}">connected services page</a>.').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)

View File

@ -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('/'),
},
},
)

View File

@ -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,
)