Fix D213 using docformatter

prospector \
  --profile-path=/home/humitos/rtfd/code/readthedocs.org \
  --profile=prospector --die-on-tool-error \
  | \
  grep -e "^[a-z] \
  > \
  list-files.txt

for x in `cat list-files.txt`; do \
  docformatter \
    --wrap-summaries=80 \
    --wrap-descriptions=80 \
    --pre-summary-newline \
    --no-blank \
    --in-place readthedocs/$x\
; done
more-gsoc
Manuel Kaufmann 2017-12-15 20:09:29 -05:00
parent 63a96d7d61
commit 4d10f2e287
50 changed files with 495 additions and 360 deletions

View File

@ -16,7 +16,8 @@ __all__ = ['VersionManager']
class VersionManagerBase(models.Manager):
"""Version manager for manager only queries
"""
Version manager for manager only queries.
For queries not suitable for the :py:cls:`VersionQuerySet`, such as create
queries.

View File

@ -1,7 +1,8 @@
"""Classes to copy files between build and web servers
"""
Classes to copy files between build and web servers.
"Syncers" copy files from the local machine, while "Pullers" copy files to
the local machine.
"Syncers" copy files from the local machine, while "Pullers" copy files to the
local machine.
"""
from __future__ import absolute_import

View File

@ -1,4 +1,5 @@
"""Contains logic for handling version slugs.
"""
Contains logic for handling version slugs.
Handling slugs for versions is not too straightforward. We need to allow some
characters which are uncommon in usual slugs. They are dots and underscores.
@ -32,8 +33,8 @@ def get_fields_with_model(cls):
"""
Replace deprecated function of the same name in Model._meta.
This replaces deprecated function (as of Django 1.10) in
Model._meta as prescrived in the Django docs.
This replaces deprecated function (as of Django 1.10) in Model._meta as
prescrived in the Django docs.
https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api
"""
return [

View File

@ -1,4 +1,4 @@
"""Django admin interface for core models"""
"""Django admin interface for core models."""
from __future__ import absolute_import
from datetime import datetime, timedelta
@ -22,7 +22,7 @@ class UserProjectInline(admin.TabularInline):
class UserProjectFilter(admin.SimpleListFilter):
"""Filter users based on project properties"""
"""Filter users based on project properties."""
parameter_name = 'project_state'
title = _('user projects')
@ -39,7 +39,8 @@ class UserProjectFilter(admin.SimpleListFilter):
)
def queryset(self, request, queryset):
"""Add filters to queryset filter
"""
Add filters to queryset filter.
``PROJECT_ACTIVE`` and ``PROJECT_BUILT`` look for versions on projects,
``PROJECT_RECENT`` looks for projects with builds in the last year

View File

@ -31,10 +31,11 @@ SINGLE_VERSION_URLCONF = getattr(
class SubdomainMiddleware(object):
"""Middleware to display docs for non-dashboard domains"""
"""Middleware to display docs for non-dashboard domains."""
def process_request(self, request):
"""Process requests for unhandled domains
"""
Process requests for unhandled domains.
If the request is not for our ``PUBLIC_DOMAIN``, or if ``PUBLIC_DOMAIN``
is not set and the request is for a subdomain on ``PRODUCTION_DOMAIN``,
@ -132,22 +133,22 @@ class SubdomainMiddleware(object):
class SingleVersionMiddleware(object):
"""Reset urlconf for requests for 'single_version' docs.
In settings.MIDDLEWARE_CLASSES, SingleVersionMiddleware must follow
after SubdomainMiddleware.
"""
Reset urlconf for requests for 'single_version' docs.
In settings.MIDDLEWARE_CLASSES, SingleVersionMiddleware must follow after
SubdomainMiddleware.
"""
def _get_slug(self, request):
"""Get slug from URLs requesting docs.
"""
Get slug from URLs requesting docs.
If URL is like '/docs/<project_name>/', we split path
and pull out slug.
If URL is subdomain or CNAME, we simply read request.slug, which is
set by SubdomainMiddleware.
"""
slug = None
if hasattr(request, 'slug'):
@ -187,16 +188,16 @@ class SingleVersionMiddleware(object):
class ProxyMiddleware(object):
"""
Middleware that sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, if the
Middleware that sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, if the.
latter is set. This is useful if you're sitting behind a reverse proxy that
causes each request's REMOTE_ADDR to be set to 127.0.0.1.
Note that this does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind
a reverse proxy that sets HTTP_X_FORWARDED_FOR automatically, do not use
this middleware. Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and
because this sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means
anybody can "fake" their IP address. Only use this when you can absolutely
trust the value of HTTP_X_FORWARDED_FOR.
causes each request's REMOTE_ADDR to be set to 127.0.0.1. Note that this
does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind a reverse proxy
that sets HTTP_X_FORWARDED_FOR automatically, do not use this middleware.
Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and because this sets
REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means anybody can "fake"
their IP address. Only use this when you can absolutely trust the value of
HTTP_X_FORWARDED_FOR.
"""
def process_request(self, request):

View File

@ -1,4 +1,4 @@
"""URL resolver for documentation"""
"""URL resolver for documentation."""
from __future__ import absolute_import
from builtins import object
@ -54,7 +54,7 @@ class ResolverBase(object):
def base_resolve_path(self, project_slug, filename, version_slug=None,
language=None, private=False, single_version=None,
subproject_slug=None, subdomain=None, cname=None):
"""Resolve a with nothing smart, just filling in the blanks"""
"""Resolve a with nothing smart, just filling in the blanks."""
# Only support `/docs/project' URLs outside our normal environment. Normally
# the path should always have a subdomain or CNAME domain
# pylint: disable=unused-argument
@ -80,7 +80,7 @@ class ResolverBase(object):
def resolve_path(self, project, filename='', version_slug=None,
language=None, single_version=None, subdomain=None,
cname=None, private=None):
"""Resolve a URL with a subset of fields defined"""
"""Resolve a URL with a subset of fields defined."""
relation = project.superprojects.first()
cname = cname or project.domains.filter(canonical=True).first()
main_language_project = project.main_language_project
@ -145,7 +145,8 @@ class ResolverBase(object):
)
def _get_canonical_project(self, project):
"""Get canonical project in the case of subproject or translations
"""
Get canonical project in the case of subproject or translations.
:type project: Project
:rtype: Project
@ -159,7 +160,7 @@ class ResolverBase(object):
return project
def _get_project_subdomain(self, project):
"""Determine canonical project domain as subdomain"""
"""Determine canonical project domain as subdomain."""
public_domain = getattr(settings, 'PUBLIC_DOMAIN', None)
if self._use_subdomain():
project = self._get_canonical_project(project)
@ -177,9 +178,10 @@ class ResolverBase(object):
def _fix_filename(self, project, filename):
"""
Force filenames that might be HTML file paths into proper URL's
Force filenames that might be HTML file paths into proper URL's.
This basically means stripping / and .html endings and then re-adding them properly.
This basically means stripping / and .html endings and then re-adding
them properly.
"""
# Bail out on non-html files
if '.' in filename and '.html' not in filename:
@ -203,7 +205,7 @@ class ResolverBase(object):
return path
def _use_subdomain(self):
"""Make decision about whether to use a subdomain to serve docs"""
"""Make decision about whether to use a subdomain to serve docs."""
use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False)
public_domain = getattr(settings, 'PUBLIC_DOMAIN', None)
return use_subdomain and public_domain is not None

View File

@ -1,4 +1,4 @@
"""Class based settings for complex settings inheritance"""
"""Class based settings for complex settings inheritance."""
from __future__ import absolute_import
from builtins import object
@ -8,11 +8,12 @@ import sys
class Settings(object):
"""Class-based settings wrapper"""
"""Class-based settings wrapper."""
@classmethod
def load_settings(cls, module_name):
"""Export class variables and properties to module namespace
"""
Export class variables and properties to module namespace.
This will export and class variable that is all upper case and doesn't
begin with ``_``. These members will be set as attributes on the module

View File

@ -50,7 +50,6 @@ Example layout
ja/
fabric -> rtd-builds/fabric/en/latest/ # single version
"""
from __future__ import absolute_import
@ -121,9 +120,8 @@ class Symlink(object):
"""
Create proper symlinks in the right order.
Since we have a small nest of directories and symlinks,
the ordering of these calls matter,
so we provide this helper to make life easier.
Since we have a small nest of directories and symlinks, the ordering of
these calls matter, so we provide this helper to make life easier.
"""
# Outside of the web root
self.symlink_cnames()
@ -138,7 +136,8 @@ class Symlink(object):
self.symlink_versions()
def symlink_cnames(self, domain=None):
"""Symlink project CNAME domains
"""
Symlink project CNAME domains.
Link from HOME/$CNAME_ROOT/<cname> ->
HOME/$WEB_ROOT/<project>
@ -164,13 +163,14 @@ class Symlink(object):
run(['ln', '-nsf', self.project.doc_path, project_cname_symlink])
def remove_symlink_cname(self, domain):
"""Remove CNAME symlink"""
"""Remove CNAME symlink."""
self._log(u"Removing symlink for CNAME {0}".format(domain.domain))
symlink = os.path.join(self.CNAME_ROOT, domain.domain)
os.unlink(symlink)
def symlink_subprojects(self):
"""Symlink project subprojects
"""
Symlink project subprojects.
Link from $WEB_ROOT/projects/<project> ->
$WEB_ROOT/<project>
@ -213,7 +213,8 @@ class Symlink(object):
os.unlink(os.path.join(self.subproject_root, subproj))
def symlink_translations(self):
"""Symlink project translations
"""
Symlink project translations.
Link from $WEB_ROOT/<project>/<language>/ ->
$WEB_ROOT/<translation>/<language>/
@ -247,7 +248,8 @@ class Symlink(object):
shutil.rmtree(to_delete)
def symlink_single_version(self):
"""Symlink project single version
"""
Symlink project single version.
Link from $WEB_ROOT/<project> ->
HOME/user_builds/<project>/rtd-builds/latest/
@ -268,7 +270,8 @@ class Symlink(object):
run(['ln', '-nsf', docs_dir, symlink])
def symlink_versions(self):
"""Symlink project's versions
"""
Symlink project's versions.
Link from $WEB_ROOT/<project>/<language>/<version>/ ->
HOME/user_builds/<project>/rtd-builds/<version>
@ -295,7 +298,7 @@ class Symlink(object):
os.unlink(os.path.join(version_dir, old_ver))
def get_default_version(self):
"""Look up project default version, return None if not found"""
"""Look up project default version, return None if not found."""
default_version = self.project.get_default_version()
try:
return self.get_version_queryset().get(slug=default_version)

View File

@ -1,4 +1,4 @@
"""Basic tasks"""
"""Basic tasks."""
from __future__ import absolute_import
import logging
@ -19,7 +19,8 @@ EMAIL_TIME_LIMIT = 30
@app.task(queue='web', time_limit=EMAIL_TIME_LIMIT)
def send_email_task(recipient, subject, template, template_html,
context=None, from_email=None, **kwargs):
"""Send multipart email
"""
Send multipart email.
recipient
Email recipient address

View File

@ -1,4 +1,4 @@
"""Common utilty functions"""
"""Common utilty functions."""
from __future__ import absolute_import
@ -77,7 +77,8 @@ def cname_to_slug(host):
def trigger_build(project, version=None, record=True, force=False, basic=False):
"""Trigger build for project and version
"""
Trigger build for project and version.
If project has a ``build_queue``, execute task on this build queue. Queue
will be prefixed with ``build-`` to unify build queue names.
@ -135,7 +136,8 @@ def trigger_build(project, version=None, record=True, force=False, basic=False):
def send_email(recipient, subject, template, template_html, context=None,
request=None, from_email=None, **kwargs): # pylint: disable=unused-argument
"""Alter context passed in and call email send task
"""
Alter context passed in and call email send task.
.. seealso::
@ -152,7 +154,8 @@ def send_email(recipient, subject, template, template_html, context=None,
def slugify(value, *args, **kwargs):
"""Add a DNS safe option to slugify
"""
Add a DNS safe option to slugify.
:param dns_safe: Remove underscores from slug as well
"""
@ -170,9 +173,9 @@ def safe_makedirs(directory_name):
"""
Safely create a directory.
Makedirs has an issue where it has a race condition around
checking for a directory and then creating it.
This catches the exception in the case where the dir already exists.
Makedirs has an issue where it has a race condition around checking for a
directory and then creating it. This catches the exception in the case where
the dir already exists.
"""
try:
os.makedirs(directory_name)

View File

@ -1,4 +1,4 @@
"""Patterns for extending Read the Docs"""
"""Patterns for extending Read the Docs."""
from __future__ import absolute_import
import inspect
@ -9,7 +9,8 @@ import six
def get_override_class(proxy_class, default_class=None):
"""Determine which class to use in an override class
"""
Determine which class to use in an override class.
The `proxy_class` is the main class that is used, and `default_class` is the
default class that this proxy class will instantiate. If `default_class` is
@ -33,7 +34,8 @@ def get_override_class(proxy_class, default_class=None):
class SettingsOverrideMeta(type):
"""Meta class for passing along classmethod class to the underlying class"""
"""Meta class for passing along classmethod class to the underlying
class."""
def __getattr__(cls, attr): # noqa: pep8 false positive
proxy_class = get_override_class(cls, getattr(cls, '_default_class'))
@ -42,7 +44,8 @@ class SettingsOverrideMeta(type):
class SettingsOverrideObject(six.with_metaclass(SettingsOverrideMeta, object)):
"""Base class for creating class that can be overridden
"""
Base class for creating class that can be overridden.
This is used for extension points in the code, where we want to extend a
class without monkey patching it. This class will proxy classmethod calls
@ -68,7 +71,8 @@ class SettingsOverrideObject(six.with_metaclass(SettingsOverrideMeta, object)):
_override_setting = None
def __new__(cls, *args, **kwargs):
"""Set up wrapped object
"""
Set up wrapped object.
Create an instance of the underlying target class and return instead of
this class.

View File

@ -107,7 +107,8 @@ def _build_url(url, projects, branches):
"""
Map a URL onto specific projects to build that are linked to that URL.
Check each of the ``branches`` to see if they are active and should be built.
Check each of the ``branches`` to see if they are active and should be
built.
"""
ret = ""
all_built = {}
@ -152,7 +153,7 @@ def _build_url(url, projects, branches):
@csrf_exempt
def github_build(request): # noqa: D205
"""
GitHub webhook consumer
GitHub webhook consumer.
.. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.GitHubWebhookView`
@ -206,7 +207,8 @@ def github_build(request): # noqa: D205
@csrf_exempt
def gitlab_build(request): # noqa: D205
"""GitLab webhook consumer
"""
GitLab webhook consumer.
.. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.GitLabWebhookView`
@ -239,7 +241,8 @@ def gitlab_build(request): # noqa: D205
@csrf_exempt
def bitbucket_build(request):
"""Consume webhooks from multiple versions of Bitbucket's API
"""
Consume webhooks from multiple versions of Bitbucket's API.
.. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.BitbucketWebhookView`
@ -307,11 +310,13 @@ def bitbucket_build(request):
@csrf_exempt
def generic_build(request, project_id_or_slug=None):
"""Generic webhook build endpoint
"""
Generic webhook build endpoint.
.. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.GenericWebhookView`
instead of this view function
Use :py:cls:`readthedocs.restapi.views.integrations.GenericWebhookView`
instead of this view function
"""
try:
project = Project.objects.get(pk=project_id_or_slug)

View File

@ -1,7 +1,7 @@
"""MkDocs_ backend for building docs.
"""
MkDocs_ backend for building docs.
.. _MkDocs: http://www.mkdocs.org/
"""
from __future__ import absolute_import
import os
@ -22,10 +22,10 @@ OVERRIDE_TEMPLATE_DIR = '%s/readthedocs/templates/mkdocs/overrides' % settings.S
def get_absolute_media_url():
"""Get the fully qualified media URL from settings.
"""
Get the fully qualified media URL from settings.
Mkdocs needs a full domain because it tries to link to local media files.
"""
media_url = settings.MEDIA_URL
@ -38,7 +38,7 @@ def get_absolute_media_url():
class BaseMkdocs(BaseBuilder):
"""Mkdocs builder"""
"""Mkdocs builder."""
use_theme = True
@ -50,10 +50,10 @@ class BaseMkdocs(BaseBuilder):
self.root_path = self.version.project.checkout_path(self.version.slug)
def load_yaml_config(self):
"""Load a YAML config.
"""
Load a YAML config.
Raise BuildEnvironmentError if failed due to syntax errors.
"""
try:
return yaml.safe_load(
@ -74,7 +74,7 @@ class BaseMkdocs(BaseBuilder):
note,))
def append_conf(self, **__):
"""Set mkdocs config values"""
"""Set mkdocs config values."""
# Pull mkdocs config data
user_config = self.load_yaml_config()

View File

@ -1,4 +1,4 @@
"""Documentation Builder Environments"""
"""Documentation Builder Environments."""
from __future__ import absolute_import
from builtins import str
@ -45,7 +45,8 @@ __all__ = (
class BuildCommand(BuildCommandResultMixin):
"""Wrap command execution for execution in build environments
"""
Wrap command execution for execution in build environments.
This wraps subprocess commands with some logic to handle exceptions,
logging, and setting up the env for the build command.
@ -101,7 +102,8 @@ class BuildCommand(BuildCommandResultMixin):
return '\n'.join([self.get_command(), output])
def run(self):
"""Set up subprocess and execute command
"""
Set up subprocess and execute command.
:param cmd_input: input to pass to command in STDIN
:type cmd_input: str
@ -170,13 +172,13 @@ class BuildCommand(BuildCommandResultMixin):
self.end_time = datetime.utcnow()
def get_command(self):
"""Flatten command"""
"""Flatten command."""
if hasattr(self.command, '__iter__') and not isinstance(self.command, str):
return ' '.join(self.command)
return self.command
def save(self):
"""Save this command and result via the API"""
"""Save this command and result via the API."""
data = {
'build': self.build_env.build.get('id'),
'command': self.get_command(),
@ -191,13 +193,15 @@ class BuildCommand(BuildCommandResultMixin):
class DockerBuildCommand(BuildCommand):
"""Create a docker container and run a command inside the container
"""
Create a docker container and run a command inside the container.
Build command to execute in docker container
"""
def run(self):
"""Execute command in existing Docker container
"""
Execute command in existing Docker container.
:param cmd_input: input to pass to command in STDIN
:type cmd_input: str
@ -241,7 +245,8 @@ class DockerBuildCommand(BuildCommand):
self.end_time = datetime.utcnow()
def get_wrapped_command(self):
"""Escape special bash characters in command to wrap in shell
"""
Escape special bash characters in command to wrap in shell.
In order to set the current working path inside a docker container, we
need to wrap the command in a shell call manually. Some characters will
@ -264,7 +269,8 @@ class DockerBuildCommand(BuildCommand):
class BuildEnvironment(object):
"""Base build environment
"""
Base build environment.
Base class for wrapping command execution for build steps. This provides a
context for command execution and reporting, and eventually performs updates
@ -320,7 +326,8 @@ class BuildEnvironment(object):
return ret
def handle_exception(self, exc_type, exc_value, _):
"""Exception handling for __enter__ and __exit__
"""
Exception handling for __enter__ and __exit__
This reports on the exception we're handling and special cases
subclasses of BuildEnvironmentException. For
@ -340,11 +347,12 @@ class BuildEnvironment(object):
return True
def run(self, *cmd, **kwargs):
"""Shortcut to run command from environment"""
"""Shortcut to run command from environment."""
return self.run_command_class(cls=self.command_class, cmd=cmd, **kwargs)
def run_command_class(self, cls, cmd, **kwargs):
"""Run command from this environment
"""
Run command from this environment.
Use ``cls`` to instantiate a command
@ -383,13 +391,14 @@ class BuildEnvironment(object):
@property
def successful(self):
"""Is build completed, without top level failures or failing commands"""
"""Is build completed, without top level failures or failing
commands."""
return (self.done and self.failure is None and
all(cmd.successful for cmd in self.commands))
@property
def failed(self):
"""Is build completed, but has top level failure or failing commands"""
"""Is build completed, but has top level failure or failing commands."""
return (self.done and (
self.failure is not None or
any(cmd.failed for cmd in self.commands)
@ -397,12 +406,13 @@ class BuildEnvironment(object):
@property
def done(self):
"""Is build in finished state"""
"""Is build in finished state."""
return (self.build is not None and
self.build['state'] == BUILD_STATE_FINISHED)
def update_build(self, state=None):
"""Record a build by hitting the API
"""
Record a build by hitting the API.
This step is skipped if we aren't recording the build. To avoid
recording successful builds yet (for instance, running setup commands
@ -488,7 +498,7 @@ class BuildEnvironment(object):
class LocalEnvironment(BuildEnvironment):
"""Local execution environment"""
"""Local execution environment."""
command_class = BuildCommand
@ -496,7 +506,7 @@ class LocalEnvironment(BuildEnvironment):
class DockerEnvironment(BuildEnvironment):
"""
Docker build environment, uses docker to contain builds
Docker build environment, uses docker to contain builds.
If :py:data:`settings.DOCKER_ENABLE` is true, build documentation inside a
docker container, instead of the host system, using this build environment
@ -532,7 +542,7 @@ class DockerEnvironment(BuildEnvironment):
self.container_time_limit = self.project.container_time_limit
def __enter__(self):
"""Start of environment context"""
"""Start of environment context."""
log.info('Creating container')
try:
# Test for existing container. We remove any stale containers that
@ -579,7 +589,7 @@ class DockerEnvironment(BuildEnvironment):
return self
def __exit__(self, exc_type, exc_value, tb):
"""End of environment context"""
"""End of environment context."""
try:
# Update buildenv state given any container error states first
self.update_build_from_container_state()
@ -624,7 +634,7 @@ class DockerEnvironment(BuildEnvironment):
return ret
def get_client(self):
"""Create Docker client connection"""
"""Create Docker client connection."""
try:
if self.client is None:
self.client = Client(
@ -652,14 +662,14 @@ class DockerEnvironment(BuildEnvironment):
@property
def container_id(self):
"""Return id of container if it is valid"""
"""Return id of container if it is valid."""
if self.container_name:
return self.container_name
elif self.container:
return self.container.get('Id')
def container_state(self):
"""Get container state"""
"""Get container state."""
client = self.get_client()
try:
info = client.inspect_container(self.container_id)
@ -668,7 +678,8 @@ class DockerEnvironment(BuildEnvironment):
return None
def update_build_from_container_state(self):
"""Update buildenv state from container state
"""
Update buildenv state from container state.
In the case of the parent command exiting before the exec commands
finish and the container is destroyed, or in the case of OOM on the
@ -689,7 +700,7 @@ class DockerEnvironment(BuildEnvironment):
.format(state.get('Error'))))
def create_container(self):
"""Create docker container"""
"""Create docker container."""
client = self.get_client()
image = self.container_image
if self.project.container_image:

View File

@ -76,7 +76,8 @@ class PythonEnvironment(object):
)
def venv_bin(self, filename=None):
"""Return path to the virtualenv bin path, or a specific binary
"""
Return path to the virtualenv bin path, or a specific binary.
:param filename: If specified, add this filename to the path return
:returns: Path to virtualenv bin or filename in virtualenv bin
@ -89,10 +90,10 @@ class PythonEnvironment(object):
class Virtualenv(PythonEnvironment):
"""A virtualenv_ environment.
"""
A virtualenv_ environment.
.. _virtualenv: https://virtualenv.pypa.io/
"""
def venv_path(self):
@ -203,10 +204,10 @@ class Virtualenv(PythonEnvironment):
class Conda(PythonEnvironment):
"""A Conda_ environment.
"""
A Conda_ environment.
.. _Conda: https://conda.io/docs/
"""
def venv_path(self):

View File

@ -1,6 +1,6 @@
"""A Django app for Gold Membership.
"""
A Django app for Gold Membership.
Gold Membership is Read the Docs' program for recurring, monthly donations.
"""
default_app_config = 'readthedocs.gold.apps.GoldAppConfig'

View File

@ -1,4 +1,4 @@
"""Gold subscription forms"""
"""Gold subscription forms."""
from __future__ import absolute_import
from builtins import object
@ -12,7 +12,8 @@ from .models import LEVEL_CHOICES, GoldUser
class GoldSubscriptionForm(StripeResourceMixin, StripeModelForm):
"""Gold subscription payment form
"""
Gold subscription payment form.
This extends the common base form for handling Stripe subscriptions. Credit
card fields for card number, expiry, and CVV are extended from

View File

@ -1,4 +1,4 @@
"""Gold subscription views"""
"""Gold subscription views."""
from __future__ import absolute_import
from django.core.urlresolvers import reverse, reverse_lazy
@ -22,7 +22,7 @@ from .models import GoldUser
class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin):
"""Gold subscription mixin for view classes"""
"""Gold subscription mixin for view classes."""
model = GoldUser
form_class = GoldSubscriptionForm
@ -34,7 +34,7 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin
return None
def get_form(self, data=None, files=None, **kwargs):
"""Pass in copy of POST data to avoid read only QueryDicts"""
"""Pass in copy of POST data to avoid read only QueryDicts."""
kwargs['customer'] = self.request.user
return super(GoldSubscriptionMixin, self).get_form(data, files, **kwargs)
@ -57,7 +57,8 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin
class DetailGoldSubscription(GoldSubscriptionMixin, DetailView):
def get(self, request, *args, **kwargs):
"""GET handling for this view
"""
GET handling for this view.
If there is a gold subscription instance, then we show the normal detail
page, otherwise show the registration form
@ -74,7 +75,8 @@ class UpdateGoldSubscription(GoldSubscriptionMixin, UpdateView):
class DeleteGoldSubscription(GoldSubscriptionMixin, DeleteView):
"""Delete Gold subscription view
"""
Delete Gold subscription view.
On object deletion, the corresponding Stripe customer is deleted as well.
Deletion is triggered on subscription deletion using a signal, ensuring the
@ -84,7 +86,7 @@ class DeleteGoldSubscription(GoldSubscriptionMixin, DeleteView):
success_message = _('Your subscription has been cancelled')
def post(self, request, *args, **kwargs):
"""Add success message to delete post"""
"""Add success message to delete post."""
resp = super(DeleteGoldSubscription, self).post(request, *args, **kwargs)
success_message = self.get_success_message({})
if success_message:

View File

@ -1,4 +1,4 @@
"""Integration admin models"""
"""Integration admin models."""
from __future__ import absolute_import
from django.contrib import admin
@ -30,7 +30,8 @@ def pretty_json_field(field, description, include_styles=False):
class HttpExchangeAdmin(admin.ModelAdmin):
"""Admin model for HttpExchange
"""
Admin model for HttpExchange.
This adds some read-only display to the admin model.
"""
@ -78,7 +79,8 @@ class HttpExchangeAdmin(admin.ModelAdmin):
class IntegrationAdmin(admin.ModelAdmin):
"""Admin model for Integration
"""
Admin model for Integration.
Because of some problems using JSONField with admin model inlines, this
instead just links to the queryset.
@ -88,7 +90,8 @@ class IntegrationAdmin(admin.ModelAdmin):
readonly_fields = ['exchanges']
def exchanges(self, obj):
"""Manually make an inline-ish block
"""
Manually make an inline-ish block.
JSONField doesn't do well with fieldsets for whatever reason. This is
just to link to the exchanges.

View File

@ -1,8 +1,9 @@
"""Integration utility functions"""
"""Integration utility functions."""
def normalize_request_payload(request):
"""Normalize the request body, hopefully to JSON
"""
Normalize the request body, hopefully to JSON.
This will attempt to return a JSON body, backing down to a string body next.

View File

@ -1,4 +1,5 @@
"""Extensions to Django messages to support notifications to users.
"""
Extensions to Django messages to support notifications to users.
Notifications are important communications to users that need to be as visible
as possible. We support different backends to make notifications visible in
@ -10,7 +11,6 @@ on the site.
.. _`django-messages-extends`: https://github.com
/AliLozano/django-messages-extends/
"""
from .notification import Notification
from .backends import send_notification

View File

@ -1,9 +1,9 @@
"""Pluggable backends for the delivery of notifications.
"""
Pluggable backends for the delivery of notifications.
Delivery of notifications to users depends on a list of backends configured in
Django settings. For example, they might be e-mailed to users as well as
displayed on the site.
"""
from __future__ import absolute_import
@ -19,7 +19,8 @@ from .constants import LEVEL_MAPPING, REQUIREMENT, HTML
def send_notification(request, notification):
"""Send notifications through all backends defined by settings
"""
Send notifications through all backends defined by settings.
Backends should be listed in the settings ``NOTIFICATION_BACKENDS``, which
should be a list of class paths to be loaded, using the standard Django
@ -42,7 +43,8 @@ class Backend(object):
class EmailBackend(Backend):
"""Send templated notification emails through our standard email backend
"""
Send templated notification emails through our standard email backend.
The content body is first rendered from an on-disk template, then passed
into the standard email templates as a string.
@ -66,7 +68,8 @@ class EmailBackend(Backend):
class SiteBackend(Backend):
"""Add messages through Django messages application
"""
Add messages through Django messages application.
This uses persistent messageing levels provided by :py:mod:`message_extends`
and stores persistent messages in the database.

View File

@ -6,7 +6,8 @@ from django.utils.translation import ugettext_lazy as _
class SendNotificationForm(forms.Form):
"""Send notification form
"""
Send notification form.
Used for sending a notification to a list of users from admin pages
@ -33,7 +34,7 @@ class SendNotificationForm(forms.Form):
in self.notification_classes]
def clean_source(self):
"""Get the source class from the class name"""
"""Get the source class from the class name."""
source = self.cleaned_data['source']
classes = dict((cls.name, cls) for cls in self.notification_classes)
return classes.get(source, None)

View File

@ -13,14 +13,14 @@ from . import constants
class Notification(object):
"""An unsent notification linked to an object.
"""
An unsent notification linked to an object.
This class provides an interface to construct notification messages by
rendering Django templates. The ``Notification`` itself is not expected
to be persisted by the backends.
Call .send() to send the notification.
"""
name = None
@ -75,7 +75,8 @@ class Notification(object):
)
def send(self):
"""Trigger notification send through all notification backends
"""
Trigger notification send through all notification backends.
In order to limit which backends a notification will send out from,
override this method and duplicate the logic from

View File

@ -10,7 +10,8 @@ from messages_extends.constants import PERSISTENT_MESSAGE_LEVELS
class FallbackUniqueStorage(FallbackStorage):
"""Persistent message fallback storage, but only stores unique notifications
"""
Persistent message fallback storage, but only stores unique notifications.
This loops through all backends to find messages to store, but will skip
this step if the message already exists for the user in the database.

View File

@ -9,7 +9,8 @@ from .forms import SendNotificationForm
class SendNotificationView(FormView):
"""Form view for sending notifications to users from admin pages
"""
Form view for sending notifications to users from admin pages.
Accepts the following additional parameters:
@ -28,7 +29,8 @@ class SendNotificationView(FormView):
notification_classes = []
def get_form_kwargs(self):
"""Override form kwargs based on input fields
"""
Override form kwargs based on input fields.
The admin posts to this view initially, so detect the send button on
form post variables. Drop additional fields if we see the send button.
@ -41,14 +43,14 @@ class SendNotificationView(FormView):
return kwargs
def get_initial(self):
"""Add selected ids to initial form data"""
"""Add selected ids to initial form data."""
initial = super(SendNotificationView, self).get_initial()
initial['_selected_action'] = self.request.POST.getlist(
admin.ACTION_CHECKBOX_NAME)
return initial
def form_valid(self, form):
"""If form is valid, send notification to recipients"""
"""If form is valid, send notification to recipients."""
count = 0
notification_cls = form.cleaned_data['source']
for obj in self.get_queryset().all():
@ -65,7 +67,8 @@ class SendNotificationView(FormView):
return HttpResponseRedirect(self.request.get_full_path())
def get_object_recipients(self, obj):
"""Iterate over queryset objects and return User objects
"""
Iterate over queryset objects and return User objects.
This allows for non-User querysets to pass back a list of Users to send
to. By default, assume we're working with :py:class:`User` objects and
@ -85,7 +88,7 @@ class SendNotificationView(FormView):
return self.kwargs.get('queryset')
def get_context_data(self, **kwargs):
"""Return queryset in context"""
"""Return queryset in context."""
context = super(SendNotificationView, self).get_context_data(**kwargs)
recipients = []
for obj in self.get_queryset().all():
@ -96,7 +99,8 @@ class SendNotificationView(FormView):
def message_user(self, message, level=messages.INFO, extra_tags='',
fail_silently=False):
"""Implementation of :py:meth:`django.contrib.admin.options.ModelAdmin.message_user`
"""
Implementation of :py:meth:`django.contrib.admin.options.ModelAdmin.message_user`
Send message through messages framework
"""

View File

@ -1,4 +1,4 @@
"""OAuth utility functions"""
"""OAuth utility functions."""
from __future__ import absolute_import
from builtins import str
@ -26,7 +26,7 @@ log = logging.getLogger(__name__)
class BitbucketService(Service):
"""Provider service for Bitbucket"""
"""Provider service for Bitbucket."""
adapter = BitbucketOAuth2Adapter
# TODO replace this with a less naive check
@ -34,12 +34,12 @@ class BitbucketService(Service):
https_url_pattern = re.compile(r'^https:\/\/[^@]+@bitbucket.org/')
def sync(self):
"""Sync repositories and teams from Bitbucket API"""
"""Sync repositories and teams from Bitbucket API."""
self.sync_repositories()
self.sync_teams()
def sync_repositories(self):
"""Sync repositories from Bitbucket API"""
"""Sync repositories from Bitbucket API."""
# Get user repos
try:
repos = self.paginate(
@ -71,7 +71,7 @@ class BitbucketService(Service):
pass
def sync_teams(self):
"""Sync Bitbucket teams and team repositories"""
"""Sync Bitbucket teams and team repositories."""
try:
teams = self.paginate(
'https://api.bitbucket.org/2.0/teams/?role=member'
@ -89,7 +89,8 @@ class BitbucketService(Service):
def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL,
organization=None):
"""Update or create a repository from Bitbucket API response
"""
Update or create a repository from Bitbucket API response.
.. note::
The :py:data:`admin` property is not set during creation, as
@ -145,7 +146,8 @@ class BitbucketService(Service):
fields['name'])
def create_organization(self, fields):
"""Update or create remote organization from Bitbucket API response
"""
Update or create remote organization from Bitbucket API response.
:param fields: dictionary response of data from API
:rtype: RemoteOrganization
@ -171,7 +173,7 @@ class BitbucketService(Service):
return response.json().get('values', [])
def get_webhook_data(self, project, integration):
"""Get webhook JSON data to post to the API"""
"""Get webhook JSON data to post to the API."""
return json.dumps({
'description': 'Read the Docs ({domain})'.format(domain=settings.PRODUCTION_DOMAIN),
'url': 'https://{domain}{path}'.format(
@ -187,7 +189,8 @@ class BitbucketService(Service):
})
def setup_webhook(self, project):
"""Set up Bitbucket project webhook for project
"""
Set up Bitbucket project webhook for project.
:param project: project to set up webhook for
:type project: Project
@ -231,7 +234,8 @@ class BitbucketService(Service):
return (False, resp)
def update_webhook(self, project, integration):
"""Update webhook integration
"""
Update webhook integration.
:param project: project to set up webhook for
:type project: Project

View File

@ -1,4 +1,4 @@
"""OAuth utility functions"""
"""OAuth utility functions."""
from __future__ import absolute_import
from builtins import str
@ -27,19 +27,19 @@ log = logging.getLogger(__name__)
class GitHubService(Service):
"""Provider service for GitHub"""
"""Provider service for GitHub."""
adapter = GitHubOAuth2Adapter
# TODO replace this with a less naive check
url_pattern = re.compile(r'github\.com')
def sync(self):
"""Sync repositories and organizations"""
"""Sync repositories and organizations."""
self.sync_repositories()
self.sync_organizations()
def sync_repositories(self):
"""Sync repositories from GitHub API"""
"""Sync repositories from GitHub API."""
repos = self.paginate('https://api.github.com/user/repos?per_page=100')
try:
for repo in repos:
@ -51,7 +51,7 @@ class GitHubService(Service):
'try reconnecting your account')
def sync_organizations(self):
"""Sync organizations from GitHub API"""
"""Sync organizations from GitHub API."""
try:
orgs = self.paginate('https://api.github.com/user/orgs')
for org in orgs:
@ -72,7 +72,8 @@ class GitHubService(Service):
def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL,
organization=None):
"""Update or create a repository from GitHub API response
"""
Update or create a repository from GitHub API response.
:param fields: dictionary of response data from API
:param privacy: privacy level to support
@ -122,7 +123,8 @@ class GitHubService(Service):
fields['name'])
def create_organization(self, fields):
"""Update or create remote organization from GitHub API response
"""
Update or create remote organization from GitHub API response.
:param fields: dictionary response of data from API
:rtype: RemoteOrganization
@ -155,7 +157,7 @@ class GitHubService(Service):
return response.json()
def get_webhook_data(self, project, integration):
"""Get webhook JSON data to post to the API"""
"""Get webhook JSON data to post to the API."""
return json.dumps({
'name': 'web',
'active': True,
@ -174,7 +176,8 @@ class GitHubService(Service):
})
def setup_webhook(self, project):
"""Set up GitHub project webhook for project
"""
Set up GitHub project webhook for project.
:param project: project to set up webhook for
:type project: Project
@ -221,7 +224,8 @@ class GitHubService(Service):
return (False, resp)
def update_webhook(self, project, integration):
"""Update webhook integration
"""
Update webhook integration.
:param project: project to set up webhook for
:type project: Project
@ -265,7 +269,7 @@ class GitHubService(Service):
@classmethod
def get_token_for_project(cls, project, force_local=False):
"""Get access token for project by iterating over project users"""
"""Get access token for project by iterating over project users."""
# TODO why does this only target GitHub?
if not getattr(settings, 'ALLOW_PRIVATE_REPOS', False):
return None

View File

@ -1,4 +1,4 @@
"""Payment forms"""
"""Payment forms."""
from __future__ import absolute_import
from builtins import str
@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
class StripeResourceMixin(object):
"""Stripe actions for resources, available as a Form mixin class"""
"""Stripe actions for resources, available as a Form mixin class."""
def ensure_stripe_resource(self, resource, attrs):
try:
@ -59,7 +59,8 @@ class StripeResourceMixin(object):
class StripeModelForm(forms.ModelForm):
"""Payment form base for Stripe interaction
"""
Payment form base for Stripe interaction.
Use this as a base class for payment forms. It includes the necessary fields
for card input and manipulates the Knockout field data bindings correctly.
@ -114,7 +115,8 @@ class StripeModelForm(forms.ModelForm):
super(StripeModelForm, self).__init__(*args, **kwargs)
def validate_stripe(self):
"""Run validation against Stripe
"""
Run validation against Stripe.
This is what will create several objects using the Stripe API. We need
to actually create the objects, as that is what will provide us with
@ -133,12 +135,12 @@ class StripeModelForm(forms.ModelForm):
return data
def clean(self):
"""Clean form to add Stripe objects via API during validation phase
"""
Clean form to add Stripe objects via API during validation phase.
This will handle ensuring a customer and subscription exist and will
raise any issues as validation errors. This is required because part
of Stripe's validation happens on the API call to establish a
subscription.
raise any issues as validation errors. This is required because part of
Stripe's validation happens on the API call to establish a subscription.
"""
cleaned_data = super(StripeModelForm, self).clean()
@ -171,7 +173,8 @@ class StripeModelForm(forms.ModelForm):
return cleaned_data
def clear_card_data(self):
"""Clear card data on validation errors
"""
Clear card data on validation errors.
This requires the form was created by passing in a mutable QueryDict
instance, see :py:class:`readthedocs.payments.mixin.StripeMixin`

View File

@ -1,4 +1,4 @@
"""Payment view mixin classes"""
"""Payment view mixin classes."""
from __future__ import absolute_import
from builtins import object
@ -7,7 +7,7 @@ from django.conf import settings
class StripeMixin(object):
"""Adds Stripe publishable key to the context data"""
"""Adds Stripe publishable key to the context data."""
def get_context_data(self, **kwargs):
context = super(StripeMixin, self).get_context_data(**kwargs)
@ -15,7 +15,8 @@ class StripeMixin(object):
return context
def get_form(self, data=None, files=None, **kwargs):
"""Pass in copy of POST data to avoid read only QueryDicts on form
"""
Pass in copy of POST data to avoid read only QueryDicts on form.
This is used to be able to reset some important credit card fields if
card validation fails. In this case, the Stripe token was valid, but the

View File

@ -1,4 +1,5 @@
"""Payment utility functions
"""
Payment utility functions.
These are mostly one-off functions. Define the bulk of Stripe operations on
:py:class:`readthedocs.payments.forms.StripeResourceMixin`.
@ -12,7 +13,7 @@ stripe.api_key = getattr(settings, 'STRIPE_SECRET', None)
def delete_customer(customer_id):
"""Delete customer from Stripe, cancelling subscriptions"""
"""Delete customer from Stripe, cancelling subscriptions."""
try:
customer = stripe.Customer.retrieve(customer_id)
return customer.delete()
@ -21,7 +22,7 @@ def delete_customer(customer_id):
def cancel_subscription(customer_id, subscription_id):
"""Cancel Stripe subscription, if it exists"""
"""Cancel Stripe subscription, if it exists."""
try:
customer = stripe.Customer.retrieve(customer_id)
if hasattr(customer, 'subscriptions'):

View File

@ -74,7 +74,8 @@ class DomainInline(admin.TabularInline):
class ProjectOwnerBannedFilter(admin.SimpleListFilter):
"""Filter for projects with banned owners
"""
Filter for projects with banned owners.
There are problems adding `users__profile__banned` to the `list_filter`
attribute, so we'll create a basic filter to capture banned owners.
@ -98,7 +99,7 @@ class ProjectOwnerBannedFilter(admin.SimpleListFilter):
class ProjectAdmin(GuardedModelAdmin):
"""Project model admin view"""
"""Project model admin view."""
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'repo', 'repo_type', 'allow_comments', 'featured', 'theme')
@ -121,7 +122,8 @@ class ProjectAdmin(GuardedModelAdmin):
send_owner_email.short_description = 'Notify project owners'
def ban_owner(self, request, queryset):
"""Ban project owner
"""
Ban project owner.
This will only ban single owners, because a malicious user could add a
user as a co-owner of the project. We don't want to induce and
@ -146,7 +148,8 @@ class ProjectAdmin(GuardedModelAdmin):
ban_owner.short_description = 'Ban project owner'
def delete_selected_and_artifacts(self, request, queryset):
"""Remove HTML/etc artifacts from application instances
"""
Remove HTML/etc artifacts from application instances.
Prior to the query delete, broadcast tasks to delete HTML artifacts from
application instances.

View File

@ -1,4 +1,4 @@
"""Project exceptions"""
"""Project exceptions."""
from django.conf import settings
from django.utils.translation import ugettext_noop as _
@ -8,7 +8,7 @@ from readthedocs.doc_builder.exceptions import BuildEnvironmentError
class ProjectConfigurationError(BuildEnvironmentError):
"""Error raised trying to configure a project for build"""
"""Error raised trying to configure a project for build."""
NOT_FOUND = _(
'A configuration file was not found. '
@ -38,7 +38,8 @@ class RepositoryError(BuildEnvironmentError):
class ProjectSpamError(Exception):
"""Error raised when a project field has detected spam
"""
Error raised when a project field has detected spam.
This error is not raised to users, we use this for banning users in the
background.

View File

@ -1,4 +1,4 @@
"""Project models"""
"""Project models."""
from __future__ import absolute_import
@ -43,7 +43,8 @@ log = logging.getLogger(__name__)
@python_2_unicode_compatible
class ProjectRelationship(models.Model):
"""Project to project relationship
"""
Project to project relationship.
This is used for subprojects
"""
@ -72,7 +73,7 @@ class ProjectRelationship(models.Model):
@python_2_unicode_compatible
class Project(models.Model):
"""Project model"""
"""Project model."""
# Auto fields
pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True)
@ -343,7 +344,8 @@ class Project(models.Model):
return reverse('projects_detail', args=[self.slug])
def get_docs_url(self, version_slug=None, lang_slug=None, private=None):
"""Return a URL for the docs
"""
Return a URL for the docs.
Always use http for now, to avoid content warnings.
"""
@ -360,7 +362,8 @@ class Project(models.Model):
return self.get_docs_url()
def get_subproject_urls(self):
"""List subproject URLs
"""
List subproject URLs.
This is used in search result linking
"""
@ -375,7 +378,8 @@ class Project(models.Model):
def get_production_media_path(self, type_, version_slug, include_file=True):
"""
This is used to see if these files exist so we can offer them for download.
This is used to see if these files exist so we can offer them for
download.
:param type_: Media content type, ie - 'pdf', 'zip'
:param version_slug: Project version slug for lookup
@ -409,7 +413,7 @@ class Project(models.Model):
return path
def subdomain(self):
"""Get project subdomain from resolver"""
"""Get project subdomain from resolver."""
return resolve_domain(self)
def get_downloads(self):
@ -440,7 +444,7 @@ class Project(models.Model):
@property
def pip_cache_path(self):
"""Path to pip cache"""
"""Path to pip cache."""
if getattr(settings, 'GLOBAL_PIP_CACHE', False):
return settings.GLOBAL_PIP_CACHE
return os.path.join(self.doc_path, '.cache', 'pip')
@ -449,7 +453,7 @@ class Project(models.Model):
# Paths for symlinks in project doc_path.
#
def translations_symlink_path(self, language=None):
"""Path in the doc_path that we symlink translations"""
"""Path in the doc_path that we symlink translations."""
if not language:
language = self.language
return os.path.join(self.doc_path, 'translations', language)
@ -459,7 +463,7 @@ class Project(models.Model):
#
def full_doc_path(self, version=LATEST):
"""The path to the documentation root in the project"""
"""The path to the documentation root in the project."""
doc_base = self.checkout_path(version)
for possible_path in ['docs', 'doc', 'Doc']:
if os.path.exists(os.path.join(doc_base, '%s' % possible_path)):
@ -468,19 +472,19 @@ class Project(models.Model):
return doc_base
def artifact_path(self, type_, version=LATEST):
"""The path to the build html docs in the project"""
"""The path to the build html docs in the project."""
return os.path.join(self.doc_path, "artifacts", version, type_)
def full_build_path(self, version=LATEST):
"""The path to the build html docs in the project"""
"""The path to the build html docs in the project."""
return os.path.join(self.conf_dir(version), "_build", "html")
def full_latex_path(self, version=LATEST):
"""The path to the build LaTeX docs in the project"""
"""The path to the build LaTeX docs in the project."""
return os.path.join(self.conf_dir(version), "_build", "latex")
def full_epub_path(self, version=LATEST):
"""The path to the build epub docs in the project"""
"""The path to the build epub docs in the project."""
return os.path.join(self.conf_dir(version), "_build", "epub")
# There is currently no support for building man/dash formats, but we keep
@ -488,34 +492,34 @@ class Project(models.Model):
# legacy builds.
def full_man_path(self, version=LATEST):
"""The path to the build man docs in the project"""
"""The path to the build man docs in the project."""
return os.path.join(self.conf_dir(version), "_build", "man")
def full_dash_path(self, version=LATEST):
"""The path to the build dash docs in the project"""
"""The path to the build dash docs in the project."""
return os.path.join(self.conf_dir(version), "_build", "dash")
def full_json_path(self, version=LATEST):
"""The path to the build json docs in the project"""
"""The path to the build json docs in the project."""
if 'sphinx' in self.documentation_type:
return os.path.join(self.conf_dir(version), "_build", "json")
elif 'mkdocs' in self.documentation_type:
return os.path.join(self.checkout_path(version), "_build", "json")
def full_singlehtml_path(self, version=LATEST):
"""The path to the build singlehtml docs in the project"""
"""The path to the build singlehtml docs in the project."""
return os.path.join(self.conf_dir(version), "_build", "singlehtml")
def rtd_build_path(self, version=LATEST):
"""The destination path where the built docs are copied"""
"""The destination path where the built docs are copied."""
return os.path.join(self.doc_path, 'rtd-builds', version)
def static_metadata_path(self):
"""The path to the static metadata JSON settings file"""
"""The path to the static metadata JSON settings file."""
return os.path.join(self.doc_path, 'metadata.json')
def conf_file(self, version=LATEST):
"""Find a ``conf.py`` file in the project checkout"""
"""Find a ``conf.py`` file in the project checkout."""
if self.conf_py_file:
conf_path = os.path.join(self.checkout_path(version), self.conf_py_file)
if os.path.exists(conf_path):
@ -542,12 +546,12 @@ class Project(models.Model):
@property
def is_type_sphinx(self):
"""Is project type Sphinx"""
"""Is project type Sphinx."""
return 'sphinx' in self.documentation_type
@property
def is_type_mkdocs(self):
"""Is project type Mkdocs"""
"""Is project type Mkdocs."""
return 'mkdocs' in self.documentation_type
@property
@ -603,7 +607,8 @@ class Project(models.Model):
return Lock(self, version, timeout, polling_interval)
def find(self, filename, version):
"""Find files inside the project's ``doc`` path
"""
Find files inside the project's ``doc`` path.
:param filename: Filename to search for in project checkout
:param version: Version instance to set version checkout path
@ -615,7 +620,8 @@ class Project(models.Model):
return matches
def full_find(self, filename, version):
"""Find files inside a project's checkout path
"""
Find files inside a project's checkout path.
:param filename: Filename to search for in project checkout
:param version: Version instance to set version checkout path
@ -628,10 +634,9 @@ class Project(models.Model):
def get_latest_build(self, finished=True):
"""
Get latest build for project
Get latest build for project.
finished
Return only builds that are in a finished state
finished -- Return only builds that are in a finished state
"""
kwargs = {'type': 'html'}
if finished:
@ -664,7 +669,8 @@ class Project(models.Model):
return sort_version_aware(versions)
def all_active_versions(self):
"""Get queryset with all active versions
"""
Get queryset with all active versions.
.. note::
This is a temporary workaround for activate_versions filtering out
@ -675,7 +681,8 @@ class Project(models.Model):
return self.versions.filter(active=True)
def supported_versions(self):
"""Get the list of supported versions
"""
Get the list of supported versions.
:returns: List of version strings.
"""
@ -693,7 +700,8 @@ class Project(models.Model):
return self.versions.filter(slug=STABLE).first()
def update_stable_version(self):
"""Returns the version that was promoted to be the new stable version
"""
Returns the version that was promoted to be the new stable version.
Return ``None`` if no update was mode or if there is no version on the
project that can be considered stable.
@ -749,7 +757,7 @@ class Project(models.Model):
return LATEST
def get_default_branch(self):
"""Get the version representing 'latest'"""
"""Get the version representing 'latest'."""
if self.default_branch:
return self.default_branch
return self.vcs_repo().fallback_branch
@ -776,7 +784,8 @@ class Project(models.Model):
return queue
def add_node(self, content_hash, page, version, commit):
"""Add comment node
"""
Add comment node.
:param content_hash: Hash of node content
:param page: Doc page for node
@ -804,7 +813,8 @@ class Project(models.Model):
return True # ie, it's True that a new node was created.
def add_comment(self, version_slug, page, content_hash, commit, user, text):
"""Add comment to node
"""
Add comment to node.
:param version_slug: Version slug to use for node lookup
:param page: Page to attach comment to
@ -827,7 +837,8 @@ class Project(models.Model):
return Feature.objects.for_project(self)
def has_feature(self, feature_id):
"""Does project have existing feature flag
"""
Does project have existing feature flag.
If the feature has a historical True value before the feature was added,
we consider the project to have the flag. This is used for deprecating a
@ -836,7 +847,8 @@ class Project(models.Model):
return self.features.filter(feature_id=feature_id).exists()
def get_feature_value(self, feature, positive, negative):
"""Look up project feature, return corresponding value
"""
Look up project feature, return corresponding value.
If a project has a feature, return ``positive``, otherwise return
``negative``
@ -846,7 +858,8 @@ class Project(models.Model):
class APIProject(Project):
"""Project proxy model for API data deserialization
"""
Project proxy model for API data deserialization.
This replaces the pattern where API data was deserialized into a mocked
:py:cls:`Project` object. This pattern was confusing, as it was not explicit
@ -885,7 +898,8 @@ class APIProject(Project):
@python_2_unicode_compatible
class ImportedFile(models.Model):
"""Imported files model
"""
Imported files model.
This tracks files that are output from documentation builds, useful for
things like CDN invalidation.
@ -986,7 +1000,8 @@ class Domain(models.Model):
@python_2_unicode_compatible
class Feature(models.Model):
"""Project feature flags
"""
Project feature flags.
Features should generally be added here as choices, however features may
also be added dynamically from a signal in other packages. Features can be
@ -1041,7 +1056,8 @@ class Feature(models.Model):
)
def get_feature_display(self):
"""Implement display name field for fake ChoiceField
"""
Implement display name field for fake ChoiceField.
Because the field is not a ChoiceField here, we need to manually
implement this behavior.

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
"""
Project search indexes
Project search indexes.
.. deprecated::
Read the Docs no longer uses Haystack in production and the core team does
not maintain this code. Use at your own risk, this may go away soon.
Read the Docs no longer uses Haystack in production and the core team does not
maintain this code. Use at your own risk, this may go away soon.
"""
from __future__ import absolute_import
@ -26,7 +27,7 @@ log = logging.getLogger(__name__)
class ProjectIndex(indexes.SearchIndex, indexes.Indexable):
"""Project search index"""
"""Project search index."""
text = CharField(document=True, use_template=True)
author = CharField()
@ -45,14 +46,14 @@ class ProjectIndex(indexes.SearchIndex, indexes.Indexable):
return Project
def index_queryset(self, using=None):
"""Used when the entire index for model is updated"""
"""Used when the entire index for model is updated."""
return self.get_model().objects.public()
# TODO Should prob make a common subclass for this and FileIndex
class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable):
"""Search index for imported files"""
"""Search index for imported files."""
text = CharField(document=True)
author = CharField()
@ -71,7 +72,8 @@ class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable):
return obj.get_absolute_url()
def prepare_text(self, obj):
"""Prepare the text of the html file
"""
Prepare the text of the html file.
This only works on machines that have the html files for the projects
checked out.
@ -107,6 +109,6 @@ class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable):
return ImportedFile
def index_queryset(self, using=None):
"""Used when the entire index for model is updated"""
"""Used when the entire index for model is updated."""
return (self.get_model().objects
.filter(project__privacy_level=constants.PUBLIC))

View File

@ -1,4 +1,5 @@
"""Tasks related to projects
"""
Tasks related to projects.
This includes fetching repository code, cleaning ``conf.py`` files, and
rebuilding documentation.
@ -186,7 +187,8 @@ class UpdateDocsTask(Task):
return True
def run_setup(self, record=True):
"""Run setup in the local environment.
"""
Run setup in the local environment.
Return True if successful.
"""
@ -236,11 +238,11 @@ class UpdateDocsTask(Task):
return True
def run_build(self, docker=False, record=True):
"""Build the docs in an environment.
"""
Build the docs in an environment.
If `docker` is True, or Docker is enabled by the settings.DOCKER_ENABLE
setting, then build in a Docker environment. Otherwise build locally.
"""
env_vars = self.get_env_vars()
@ -294,13 +296,13 @@ class UpdateDocsTask(Task):
@staticmethod
def get_project(project_pk):
"""Get project from API"""
"""Get project from API."""
project_data = api_v2.project(project_pk).get()
return APIProject(**project_data)
@staticmethod
def get_version(project, version_pk):
"""Ensure we're using a sane version"""
"""Ensure we're using a sane version."""
if version_pk:
version_data = api_v2.version(version_pk).get()
else:
@ -312,7 +314,7 @@ class UpdateDocsTask(Task):
@staticmethod
def get_build(build_pk):
"""
Retrieve build object from API
Retrieve build object from API.
:param build_pk: Build primary key
"""
@ -369,7 +371,7 @@ class UpdateDocsTask(Task):
def update_documentation_type(self):
"""
Force Sphinx for 'auto' documentation type
Force Sphinx for 'auto' documentation type.
This used to determine the type and automatically set the documentation
type to Sphinx for rST and Mkdocs for markdown. It now just forces
@ -383,7 +385,8 @@ class UpdateDocsTask(Task):
def update_app_instances(self, html=False, localmedia=False, search=False,
pdf=False, epub=False):
"""Update application instances with build artifacts
"""
Update application instances with build artifacts.
This triggers updates across application instances for html, pdf, epub,
downloads, and search. Tasks are broadcast to all web servers from here.
@ -439,7 +442,8 @@ class UpdateDocsTask(Task):
self.python_env.install_package()
def build_docs(self):
"""Wrapper to all build functions
"""
Wrapper to all build functions.
Executes the necessary builds for this task and returns whether the
build was successful or not.
@ -465,7 +469,7 @@ class UpdateDocsTask(Task):
return outcomes
def build_docs_html(self):
"""Build HTML docs"""
"""Build HTML docs."""
html_builder = get_builder_class(self.project.documentation_type)(
build_env=self.build_env,
python_env=self.python_env,
@ -489,7 +493,7 @@ class UpdateDocsTask(Task):
return success
def build_docs_search(self):
"""Build search data with separate build"""
"""Build search data with separate build."""
if self.build_search:
if self.project.is_type_mkdocs:
return self.build_docs_class('mkdocs_json')
@ -498,7 +502,7 @@ class UpdateDocsTask(Task):
return False
def build_docs_localmedia(self):
"""Get local media files with separate build"""
"""Get local media files with separate build."""
if 'htmlzip' not in self.config.formats:
return False
@ -508,7 +512,7 @@ class UpdateDocsTask(Task):
return False
def build_docs_pdf(self):
"""Build PDF docs"""
"""Build PDF docs."""
if ('pdf' not in self.config.formats or
self.project.slug in HTML_ONLY or
not self.project.is_type_sphinx):
@ -516,7 +520,7 @@ class UpdateDocsTask(Task):
return self.build_docs_class('sphinx_pdf')
def build_docs_epub(self):
"""Build ePub docs"""
"""Build ePub docs."""
if ('epub' not in self.config.formats or
self.project.slug in HTML_ONLY or
not self.project.is_type_sphinx):
@ -524,7 +528,8 @@ class UpdateDocsTask(Task):
return self.build_docs_class('sphinx_epub')
def build_docs_class(self, builder_class):
"""Build docs with additional doc backends
"""
Build docs with additional doc backends.
These steps are not necessarily required for the build to halt, so we
only raise a warning exception here. A hard error will halt the build
@ -536,14 +541,14 @@ class UpdateDocsTask(Task):
return success
def send_notifications(self):
"""Send notifications on build failure"""
"""Send notifications on build failure."""
send_notifications.delay(self.version.pk, build_pk=self.build['id'])
@app.task()
def update_imported_docs(version_pk):
"""
Check out or update the given project's repository
Check out or update the given project's repository.
:param version_pk: Version id to update
"""
@ -626,10 +631,11 @@ def update_imported_docs(version_pk):
@app.task(queue='web')
def sync_files(project_pk, version_pk, hostname=None, html=False,
localmedia=False, search=False, pdf=False, epub=False):
"""Sync build artifacts to application instances
"""
Sync build artifacts to application instances.
This task broadcasts from a build instance on build completion and
performs synchronization of build artifacts on each application instance.
This task broadcasts from a build instance on build completion and performs
synchronization of build artifacts on each application instance.
"""
# Clean up unused artifacts
if not pdf:
@ -658,7 +664,8 @@ def sync_files(project_pk, version_pk, hostname=None, html=False,
@app.task(queue='web')
def move_files(version_pk, hostname, html=False, localmedia=False, search=False,
pdf=False, epub=False):
"""Task to move built documentation to web servers
"""
Task to move built documentation to web servers.
:param version_pk: Version id to sync files for
:param hostname: Hostname to sync to
@ -723,7 +730,8 @@ def move_files(version_pk, hostname, html=False, localmedia=False, search=False,
@app.task(queue='web')
def update_search(version_pk, commit, delete_non_commit_files=True):
"""Task to update search indexes
"""
Task to update search indexes.
:param version_pk: Version id to update
:param commit: Commit that updated index
@ -814,7 +822,8 @@ def fileify(version_pk, commit):
def _manage_imported_files(version, path, commit):
"""Update imported files for version
"""
Update imported files for version.
:param version: Version instance
:param path: Path to search
@ -869,7 +878,8 @@ def send_notifications(version_pk, build_pk):
def email_notification(version, build, email):
"""Send email notifications for build failure
"""
Send email notifications for build failure.
:param version: :py:class:`Version` instance that failed
:param build: :py:class:`Build` instance that failed
@ -903,7 +913,8 @@ def email_notification(version, build, email):
def webhook_notification(version, build, hook_url):
"""Send webhook notification for project webhook
"""
Send webhook notification for project webhook.
:param version: Version instance to send hook for
:param build: Build instance that failed
@ -928,7 +939,8 @@ def webhook_notification(version, build, hook_url):
@app.task(queue='web')
def update_static_metadata(project_pk, path=None):
"""Update static metadata JSON file
"""
Update static metadata JSON file.
Metadata settings include the following project settings:
@ -979,8 +991,8 @@ def remove_dir(path):
"""
Remove a directory on the build/celery server.
This is mainly a wrapper around shutil.rmtree so that app servers
can kill things on the build server.
This is mainly a wrapper around shutil.rmtree so that app servers can kill
things on the build server.
"""
log.info("Removing %s", path)
shutil.rmtree(path, ignore_errors=True)
@ -988,7 +1000,7 @@ def remove_dir(path):
@app.task()
def clear_artifacts(version_pk):
"""Remove artifacts from the web servers"""
"""Remove artifacts from the web servers."""
version = Version.objects.get(pk=version_pk)
clear_pdf_artifacts(version)
clear_epub_artifacts(version)

View File

@ -1,4 +1,4 @@
"""Utility functions used by projects"""
"""Utility functions used by projects."""
from __future__ import absolute_import
@ -32,7 +32,8 @@ def version_from_slug(slug, version):
def find_file(filename):
"""Recursively find matching file from the current working path
"""
Recursively find matching file from the current working path.
:param file: Filename to match
:returns: A list of matching filenames.
@ -45,7 +46,8 @@ def find_file(filename):
def run(*commands):
"""Run one or more commands
"""
Run one or more commands.
Each argument in `commands` can be passed as a string or as a list. Passing
as a list is the preferred method, as space escaping is more explicit and it
@ -106,7 +108,8 @@ def run(*commands):
def safe_write(filename, contents):
"""Normalize and write to filename
"""
Normalize and write to filename.
Write ``contents`` to the given ``filename``. If the filename's
directory does not exist, it is created. Contents are written as UTF-8,

View File

@ -1,4 +1,4 @@
"""Mixin classes for project views"""
"""Mixin classes for project views."""
from __future__ import absolute_import
from builtins import object
@ -9,7 +9,8 @@ from readthedocs.projects.models import Project
class ProjectRelationMixin(object):
"""Mixin class for constructing model views for project dashboard
"""
Mixin class for constructing model views for project dashboard.
This mixin class is used for model views on models that have a relation
to the :py:cls:`Project` model.

View File

@ -1,4 +1,4 @@
"""Project views for authenticated users"""
"""Project views for authenticated users."""
from __future__ import absolute_import
import logging
@ -52,7 +52,7 @@ class PrivateViewMixin(LoginRequiredMixin):
class ProjectDashboard(PrivateViewMixin, ListView):
"""Project dashboard"""
"""Project dashboard."""
model = Project
template_name = 'projects/project_dashboard.html'
@ -75,7 +75,8 @@ class ProjectDashboard(PrivateViewMixin, ListView):
@login_required
def project_manage(__, project_slug):
"""Project management view
"""
Project management view.
Where you will have links to edit the projects' configuration, edit the
files associated with that project, etc.
@ -131,7 +132,8 @@ class ProjectAdvancedUpdate(ProjectSpamMixin, PrivateViewMixin, UpdateView):
@login_required
def project_versions(request, project_slug):
"""Project versions view
"""
Project versions view.
Shows the available versions and lets the user choose which ones he would
like to have built.
@ -161,7 +163,7 @@ def project_versions(request, project_slug):
@login_required
def project_version_detail(request, project_slug, version_slug):
"""Project version detail page"""
"""Project version detail page."""
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),
@ -189,7 +191,8 @@ def project_version_detail(request, project_slug, version_slug):
@login_required
def project_delete(request, project_slug):
"""Project delete confirmation view
"""
Project delete confirmation view.
Make a project as deleted on POST, otherwise show a form asking for
confirmation of delete.
@ -213,14 +216,14 @@ def project_delete(request, project_slug):
class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
"""Project import wizard"""
"""Project import wizard."""
form_list = [('basics', ProjectBasicsForm),
('extra', ProjectExtraForm)]
condition_dict = {'extra': lambda self: self.is_advanced()}
def get_form_kwargs(self, step=None):
"""Get args to pass into form instantiation"""
"""Get args to pass into form instantiation."""
kwargs = {}
kwargs['user'] = self.request.user
if step == 'basics':
@ -228,11 +231,12 @@ class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
return kwargs
def get_template_names(self):
"""Return template names based on step name"""
"""Return template names based on step name."""
return 'projects/import_{0}.html'.format(self.steps.current)
def done(self, form_list, **kwargs):
"""Save form data as object instance
"""
Save form data as object instance.
Don't save form data directly, instead bypass documentation building and
other side effects for now, by signalling a save without commit. Then,
@ -260,14 +264,14 @@ class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
args=[project.slug]))
def is_advanced(self):
"""Determine if the user selected the `show advanced` field"""
"""Determine if the user selected the `show advanced` field."""
data = self.get_cleaned_data_for_step('basics') or {}
return data.get('advanced', True)
class ImportDemoView(PrivateViewMixin, View):
"""View to pass request on to import form to import demo project"""
"""View to pass request on to import form to import demo project."""
form_class = ProjectBasicsForm
request = None
@ -275,7 +279,7 @@ class ImportDemoView(PrivateViewMixin, View):
kwargs = None
def get(self, request, *args, **kwargs):
"""Process link request as a form post to the project import form"""
"""Process link request as a form post to the project import form."""
self.request = request
self.args = args
self.kwargs = kwargs
@ -305,7 +309,7 @@ class ImportDemoView(PrivateViewMixin, View):
args=[project.slug]))
def get_form_data(self):
"""Get form data to post to import form"""
"""Get form data to post to import form."""
return {
'name': '{0}-demo'.format(self.request.user.username),
'repo_type': 'git',
@ -313,13 +317,14 @@ class ImportDemoView(PrivateViewMixin, View):
}
def get_form_kwargs(self):
"""Form kwargs passed in during instantiation"""
"""Form kwargs passed in during instantiation."""
return {'user': self.request.user}
class ImportView(PrivateViewMixin, TemplateView):
"""On GET, show the source an import view, on POST, mock out a wizard
"""
On GET, show the source an import view, on POST, mock out a wizard.
If we are accepting POST data, use the fields to seed the initial data in
:py:class:`ImportWizardView`. The import templates will redirect the form to
@ -330,7 +335,8 @@ class ImportView(PrivateViewMixin, TemplateView):
wizard_class = ImportWizardView
def get(self, request, *args, **kwargs):
"""Display list of repositories to import
"""
Display list of repositories to import.
Adds a warning to the listing if any of the accounts connected for the
user are not supported accounts.
@ -377,7 +383,7 @@ class ImportView(PrivateViewMixin, TemplateView):
@login_required
def edit_alias(request, project_slug, alias_id=None):
"""Edit project alias form view"""
"""Edit project alias form view."""
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)
@ -455,7 +461,7 @@ class ProjectRelationshipDelete(ProjectRelationshipMixin, DeleteView):
@login_required
def project_users(request, project_slug):
"""Project users view and form view"""
"""Project users view and form view."""
project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug)
@ -490,7 +496,7 @@ def project_users_delete(request, project_slug):
@login_required
def project_notifications(request, project_slug):
"""Project notification view and form view"""
"""Project notification view and form view."""
project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug)
@ -538,7 +544,7 @@ def project_comments_settings(request, project_slug):
@login_required
def project_notifications_delete(request, project_slug):
"""Project notifications delete confirmation view"""
"""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),
@ -556,7 +562,7 @@ def project_notifications_delete(request, project_slug):
@login_required
def project_translations(request, project_slug):
"""Project translations view and form view"""
"""Project translations view and form view."""
project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug)
form = TranslationForm(data=request.POST or None, parent=project)
@ -587,7 +593,7 @@ def project_translations_delete(request, project_slug, child_slug):
@login_required
def project_redirects(request, project_slug):
"""Project redirects view and form view"""
"""Project redirects view and form view."""
project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug)
@ -609,7 +615,7 @@ def project_redirects(request, project_slug):
@login_required
def project_redirects_delete(request, project_slug):
"""Project redirect delete view"""
"""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),
@ -626,7 +632,8 @@ def project_redirects_delete(request, project_slug):
@login_required
def project_version_delete_html(request, project_slug, version_slug):
"""Project version 'delete' HTML
"""
Project version 'delete' HTML.
This marks a version as not built
"""
@ -672,7 +679,7 @@ class DomainDelete(DomainMixin, DeleteView):
class IntegrationMixin(ProjectAdminMixin, PrivateViewMixin):
"""Project external service mixin for listing webhook objects"""
"""Project external service mixin for listing webhook objects."""
model = Integration
integration_url_field = 'integration_pk'
@ -689,7 +696,7 @@ class IntegrationMixin(ProjectAdminMixin, PrivateViewMixin):
return self.model.objects.filter(project=self.project)
def get_integration(self):
"""Return project integration determined by url kwarg"""
"""Return project integration determined by url kwarg."""
if self.integration_url_field not in self.kwargs:
return None
return get_object_or_404(
@ -765,7 +772,8 @@ class IntegrationExchangeDetail(IntegrationMixin, DetailView):
class IntegrationWebhookSync(IntegrationMixin, GenericView):
"""Resync a project webhook
"""
Resync a project webhook.
The signal will add a success/failure message on the request.
"""

View File

@ -1,4 +1,4 @@
"""Public project views"""
"""Public project views."""
from __future__ import absolute_import
from collections import OrderedDict
@ -38,7 +38,7 @@ mimetypes.add_type("application/epub+zip", ".epub")
class ProjectIndex(ListView):
"""List view of public :py:class:`Project` instances"""
"""List view of public :py:class:`Project` instances."""
model = Project
@ -70,7 +70,7 @@ project_index = ProjectIndex.as_view()
class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):
"""Display project onboard steps"""
"""Display project onboard steps."""
model = Project
slug_url_kwarg = 'project_slug'
@ -106,7 +106,7 @@ class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):
@never_cache
def project_badge(request, project_slug):
"""Return a sweet badge for the project"""
"""Return a sweet badge for the project."""
badge_path = "projects/badges/%s.svg"
version_slug = request.GET.get('version', LATEST)
try:
@ -128,7 +128,7 @@ def project_badge(request, project_slug):
def project_downloads(request, project_slug):
"""A detail view for a project with various dataz"""
"""A detail view for a project with various dataz."""
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()
@ -158,7 +158,6 @@ def project_download_media(request, project_slug, type_, version_slug):
.. warning:: This is linked directly from the HTML pages.
It should only care about the Version permissions,
not the actual Project permissions.
"""
version = get_object_or_404(
Version.objects.public(user=request.user),
@ -189,7 +188,7 @@ def project_download_media(request, project_slug, type_, version_slug):
def search_autocomplete(request):
"""Return a json list of project names"""
"""Return a json list of project names."""
if 'term' in request.GET:
term = request.GET['term']
else:
@ -208,7 +207,7 @@ def search_autocomplete(request):
def version_autocomplete(request, project_slug):
"""Return a json list of version names"""
"""Return a json list of version names."""
queryset = Project.objects.public(request.user)
get_object_or_404(queryset, slug=project_slug)
versions = Version.objects.public(request.user)
@ -247,7 +246,7 @@ def version_filter_autocomplete(request, project_slug):
def file_autocomplete(request, project_slug):
"""Return a json list of file names"""
"""Return a json list of file names."""
if 'term' in request.GET:
term = request.GET['term']
else:
@ -266,7 +265,7 @@ def file_autocomplete(request, project_slug):
def elastic_project_search(request, project_slug):
"""Use elastic search to search in a project"""
"""Use elastic search to search in a project."""
queryset = Project.objects.protected(request.user)
project = get_object_or_404(queryset, slug=project_slug)
version_slug = request.GET.get('version', LATEST)
@ -340,7 +339,8 @@ def elastic_project_search(request, project_slug):
def project_versions(request, project_slug):
"""Project version list view
"""
Project version list view.
Shows the available versions and lets the user choose which ones to build.
"""
@ -371,7 +371,7 @@ def project_versions(request, project_slug):
def project_analytics(request, project_slug):
"""Have a analytics API placeholder"""
"""Have a analytics API placeholder."""
project = get_object_or_404(Project.objects.protected(request.user),
slug=project_slug)
analytics_cache = cache.get('analytics:%s' % project_slug)
@ -416,7 +416,7 @@ def project_analytics(request, project_slug):
def project_embed(request, project_slug):
"""Have a content API placeholder"""
"""Have a content API placeholder."""
project = get_object_or_404(Project.objects.protected(request.user),
slug=project_slug)
version = project.versions.get(slug=LATEST)

View File

@ -1,11 +1,11 @@
"""Redirection view support.
"""
Redirection view support.
This module allows for parsing a URL path, looking up redirects associated
with it in the database, and generating a redirect response.
These are not used directly as views; they are instead included into 404
handlers, so that redirects only take effect if no other view matches.
"""
from __future__ import absolute_import
from django.http import HttpResponseRedirect
@ -20,7 +20,8 @@ log = logging.getLogger(__name__)
def project_and_path_from_request(request, path):
"""Parse the project from a request path.
"""
Parse the project from a request path.
Return a tuple (project, path) where `project` is a projects.Project if
a matching project exists, and `path` is the unmatched remainder of the
@ -28,7 +29,6 @@ def project_and_path_from_request(request, path):
If the path does not match, or no matching project is found, then `project`
will be ``None``.
"""
if hasattr(request, 'slug'):
project_slug = request.slug

View File

@ -62,12 +62,13 @@ class APIPermission(permissions.IsAuthenticatedOrReadOnly):
class APIRestrictedPermission(permissions.BasePermission):
"""Allow admin write, authenticated and anonymous read only
"""
Allow admin write, authenticated and anonymous read only.
This differs from :py:class:`APIPermission` by not allowing for authenticated
POSTs. This permission is endpoints like ``/api/v2/build/``, which are used
by admin users to coordinate build instance creation, but only should be
readable by end users.
This differs from :py:class:`APIPermission` by not allowing for
authenticated POSTs. This permission is endpoints like ``/api/v2/build/``,
which are used by admin users to coordinate build instance creation, but
only should be readable by end users.
"""
def has_permission(self, request, view):

View File

@ -28,7 +28,8 @@ class ProjectSerializer(serializers.ModelSerializer):
class ProjectAdminSerializer(ProjectSerializer):
"""Project serializer for admin only access
"""
Project serializer for admin only access.
Includes special internal fields that don't need to be exposed through the
general API, mostly for fields used in the build process
@ -77,7 +78,7 @@ class VersionSerializer(serializers.ModelSerializer):
class VersionAdminSerializer(VersionSerializer):
"""Version serializer that returns admin project data"""
"""Version serializer that returns admin project data."""
project = ProjectAdminSerializer()
@ -93,7 +94,7 @@ class BuildCommandSerializer(serializers.ModelSerializer):
class BuildSerializer(serializers.ModelSerializer):
"""Build serializer for user display, doesn't display internal fields"""
"""Build serializer for user display, doesn't display internal fields."""
commands = BuildCommandSerializer(many=True, read_only=True)
state_display = serializers.ReadOnlyField(source='get_state_display')
@ -105,7 +106,7 @@ class BuildSerializer(serializers.ModelSerializer):
class BuildAdminSerializer(BuildSerializer):
"""Build serializer for display to admin users and build instances"""
"""Build serializer for display to admin users and build instances."""
class Meta(BuildSerializer.Meta):
exclude = ()
@ -142,7 +143,7 @@ class RemoteOrganizationSerializer(serializers.ModelSerializer):
class RemoteRepositorySerializer(serializers.ModelSerializer):
"""Remote service repository serializer"""
"""Remote service repository serializer."""
organization = RemoteOrganizationSerializer()
matches = serializers.SerializerMethodField()

View File

@ -78,7 +78,8 @@ def delete_versions(project, version_data):
def index_search_request(version, page_list, commit, project_scale, page_scale,
section=True, delete=True):
"""Update search indexes with build output JSON
"""
Update search indexes with build output JSON.
In order to keep sub-projects all indexed on the same shard, indexes will be
updated using the parent project's slug as the routing value.

View File

@ -39,7 +39,7 @@ class WebhookMixin(object):
integration_type = None
def post(self, request, project_slug):
"""Set up webhook post view with request and project objects"""
"""Set up webhook post view with request and project objects."""
self.request = request
self.project = None
try:
@ -57,7 +57,7 @@ class WebhookMixin(object):
return Project.objects.get(**kwargs)
def finalize_response(self, req, *args, **kwargs):
"""If the project was set on POST, store an HTTP exchange"""
"""If the project was set on POST, store an HTTP exchange."""
resp = super(WebhookMixin, self).finalize_response(req, *args, **kwargs)
if hasattr(self, 'project') and self.project:
HttpExchange.objects.from_exchange(
@ -69,15 +69,16 @@ class WebhookMixin(object):
return resp
def get_data(self):
"""Normalize posted data"""
"""Normalize posted data."""
return normalize_request_payload(self.request)
def handle_webhook(self):
"""Handle webhook payload"""
"""Handle webhook payload."""
raise NotImplementedError
def get_integration(self):
"""Get or create an inbound webhook to track webhook requests
"""
Get or create an inbound webhook to track webhook requests.
We shouldn't need this, but to support legacy webhooks, we can't assume
that a webhook has ever been created on our side. Most providers don't
@ -97,7 +98,8 @@ class WebhookMixin(object):
return integration
def get_response_push(self, project, branches):
"""Build branches on push events and return API response
"""
Build branches on push events and return API response.
Return a JSON response with the following::
@ -124,7 +126,8 @@ class WebhookMixin(object):
class GitHubWebhookView(WebhookMixin, APIView):
"""Webhook consumer for GitHub
"""
Webhook consumer for GitHub.
Accepts webhook events from GitHub, 'push' events trigger builds. Expects the
webhook event type will be included in HTTP header ``X-GitHub-Event``, and
@ -164,7 +167,8 @@ class GitHubWebhookView(WebhookMixin, APIView):
class GitLabWebhookView(WebhookMixin, APIView):
"""Webhook consumer for GitLab
"""
Webhook consumer for GitLab.
Accepts webhook events from GitLab, 'push' events trigger builds.
@ -195,7 +199,8 @@ class GitLabWebhookView(WebhookMixin, APIView):
class BitbucketWebhookView(WebhookMixin, APIView):
"""Webhook consumer for Bitbucket
"""
Webhook consumer for Bitbucket.
Accepts webhook events from Bitbucket, 'repo:push' events trigger builds.
@ -236,7 +241,8 @@ class BitbucketWebhookView(WebhookMixin, APIView):
class IsAuthenticatedOrHasToken(permissions.IsAuthenticated):
"""Allow authenticated users and requests with token auth through
"""
Allow authenticated users and requests with token auth through.
This does not check for instance-level permissions, as the check uses
methods from the view to determine if the token matches.
@ -250,7 +256,8 @@ class IsAuthenticatedOrHasToken(permissions.IsAuthenticated):
class APIWebhookView(WebhookMixin, APIView):
"""API webhook consumer
"""
API webhook consumer.
Expects the following JSON::
@ -263,7 +270,8 @@ class APIWebhookView(WebhookMixin, APIView):
permission_classes = [IsAuthenticatedOrHasToken]
def get_project(self, **kwargs):
"""Get authenticated user projects, or token authed projects
"""
Get authenticated user projects, or token authed projects.
Allow for a user to either be authed to receive a project, or require
the integration token to be specified as a POST argument.
@ -304,7 +312,8 @@ class APIWebhookView(WebhookMixin, APIView):
class WebhookView(APIView):
"""This is the main webhook view for webhooks with an ID
"""
This is the main webhook view for webhooks with an ID.
The handling of each view is handed off to another view. This should only
ever get webhook requests for established webhooks on our side. The other
@ -320,7 +329,7 @@ class WebhookView(APIView):
}
def post(self, request, project_slug, integration_pk):
"""Set up webhook post view with request and project objects"""
"""Set up webhook post view with request and project objects."""
integration = get_object_or_404(
Integration,
project__slug=project_slug,

View File

@ -34,7 +34,8 @@ log = logging.getLogger(__name__)
class UserSelectViewSet(viewsets.ModelViewSet):
"""View set that varies serializer class based on request user credentials
"""
View set that varies serializer class based on request user credentials.
Viewsets using this class should have an attribute `admin_serializer_class`,
which is a serializer that might have more fields that only admin/staff
@ -50,13 +51,15 @@ class UserSelectViewSet(viewsets.ModelViewSet):
return self.serializer_class
def get_queryset(self):
"""Use our API manager method to determine authorization on queryset"""
"""Use our API manager method to determine authorization on queryset."""
return self.model.objects.api(self.request.user)
class ProjectViewSet(UserSelectViewSet):
"""List, filter, etc. Projects."""
"""
List, filter, etc. Projects.
"""
permission_classes = [APIPermission]
renderer_classes = (JSONRenderer,)
@ -131,7 +134,8 @@ class ProjectViewSet(UserSelectViewSet):
@decorators.detail_route(permission_classes=[permissions.IsAdminUser], methods=['post'])
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.
"""

View File

@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
@decorators.permission_classes((permissions.IsAdminUser,))
@decorators.renderer_classes((JSONRenderer,))
def index_search(request):
"""Add things to the search index"""
"""Add things to the search index."""
data = request.data['data']
version_pk = data['version_pk']
commit = data.get('commit')
@ -41,7 +41,7 @@ def index_search(request):
@decorators.permission_classes((permissions.AllowAny,))
@decorators.renderer_classes((JSONRenderer,))
def search(request):
"""Perform search, supplement links by resolving project domains"""
"""Perform search, supplement links by resolving project domains."""
project_slug = request.GET.get('project', None)
version_slug = request.GET.get('version', LATEST)
query = request.GET.get('q', None)
@ -100,7 +100,8 @@ def project_search(request):
@decorators.permission_classes((permissions.AllowAny,))
@decorators.renderer_classes((JSONRenderer,))
def section_search(request):
"""Section search
"""
Section search.
Queries with query ``q`` across all documents and projects. Queries can be
limited to a single project or version by using the ``project`` and
@ -129,7 +130,6 @@ def section_search(request):
Example::
GET /api/v2/search/section/?q=virtualenv&project=django
"""
query = request.GET.get('q', None)
if not query:

View File

@ -37,20 +37,22 @@ class RTDTestCase(TestCase):
@patch('readthedocs.projects.views.private.trigger_build', lambda x, basic: None)
class MockBuildTestCase(TestCase):
"""Mock build triggers for test cases"""
"""Mock build triggers for test cases."""
pass
class RequestFactoryTestMixin(object):
"""Adds helper methods for testing with :py:class:`RequestFactory`
"""
Adds helper methods for testing with :py:class:`RequestFactory`
This handles setting up authentication, messages, and session handling
"""
def request(self, *args, **kwargs):
"""Perform request from factory
"""
Perform request from factory.
:param method: Request method as string
:returns: Request instance
@ -88,7 +90,7 @@ class RequestFactoryTestMixin(object):
class WizardTestCase(RequestFactoryTestMixin, TestCase):
"""Test case for testing wizard forms"""
"""Test case for testing wizard forms."""
step_data = OrderedDict({})
url = None
@ -97,7 +99,8 @@ class WizardTestCase(RequestFactoryTestMixin, TestCase):
@patch('readthedocs.projects.views.private.trigger_build', lambda x, basic: None)
def post_step(self, step, **kwargs):
"""Post step form data to `url`, using supplementary `kwargs`
"""
Post step form data to `url`, using supplementary `kwargs`
Use data from kwargs to build dict to pass into form
"""
@ -121,7 +124,7 @@ class WizardTestCase(RequestFactoryTestMixin, TestCase):
# We use camelCase on purpose here to conform with unittest's naming
# conventions.
def assertWizardResponse(self, response, step=None): # noqa
"""Assert successful wizard response"""
"""Assert successful wizard response."""
# This is the last form
if step is None:
try:
@ -149,7 +152,8 @@ class WizardTestCase(RequestFactoryTestMixin, TestCase):
# We use camelCase on purpose here to conform with unittest's naming
# conventions.
def assertWizardFailure(self, response, field, match=None): # noqa
"""Assert field threw a validation error
"""
Assert field threw a validation error.
response
Client response object

View File

@ -15,7 +15,7 @@ from readthedocs.search.signals import (before_project_search,
def search_project(request, query, language=None):
"""Search index for projects matching query"""
"""Search index for projects matching query."""
body = {
"query": {
"bool": {
@ -50,7 +50,8 @@ def search_project(request, query, language=None):
def search_file(request, query, project_slug=None, version_slug=LATEST, taxonomy=None):
"""Search index for files matching query
"""
Search index for files matching query.
Raises a 404 error on missing project
@ -163,7 +164,8 @@ def search_file(request, query, project_slug=None, version_slug=LATEST, taxonomy
def search_section(request, query, project_slug=None, version_slug=LATEST,
path=None):
"""Search for a section of content
"""
Search for a section of content.
When you search, you will have a ``project`` facet, which includes the
number of matching sections per project. When you search inside a project,

View File

@ -18,7 +18,7 @@ log = logging.getLogger(__name__)
def process_mkdocs_json(version, build_dir=True):
"""Given a version object, return a list of page dicts from disk content"""
"""Given a version object, return a list of page dicts from disk content."""
if build_dir:
full_path = version.project.full_json_path(version.slug)
else:
@ -215,7 +215,8 @@ def parse_sphinx_sections(content):
def parse_mkdocs_sections(content):
"""Generate a list of sections from mkdocs-style html.
"""
Generate a list of sections from mkdocs-style html.
May raise a ValueError
"""