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): 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 For queries not suitable for the :py:cls:`VersionQuerySet`, such as create
queries. 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 "Syncers" copy files from the local machine, while "Pullers" copy files to the
the local machine. local machine.
""" """
from __future__ import absolute_import 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 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. 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. Replace deprecated function of the same name in Model._meta.
This replaces deprecated function (as of Django 1.10) in This replaces deprecated function (as of Django 1.10) in Model._meta as
Model._meta as prescrived in the Django docs. prescrived in the Django docs.
https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api
""" """
return [ 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 __future__ import absolute_import
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -22,7 +22,7 @@ class UserProjectInline(admin.TabularInline):
class UserProjectFilter(admin.SimpleListFilter): class UserProjectFilter(admin.SimpleListFilter):
"""Filter users based on project properties""" """Filter users based on project properties."""
parameter_name = 'project_state' parameter_name = 'project_state'
title = _('user projects') title = _('user projects')
@ -39,7 +39,8 @@ class UserProjectFilter(admin.SimpleListFilter):
) )
def queryset(self, request, queryset): 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_ACTIVE`` and ``PROJECT_BUILT`` look for versions on projects,
``PROJECT_RECENT`` looks for projects with builds in the last year ``PROJECT_RECENT`` looks for projects with builds in the last year

View File

@ -31,10 +31,11 @@ SINGLE_VERSION_URLCONF = getattr(
class SubdomainMiddleware(object): class SubdomainMiddleware(object):
"""Middleware to display docs for non-dashboard domains""" """Middleware to display docs for non-dashboard domains."""
def process_request(self, request): 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`` 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``, is not set and the request is for a subdomain on ``PRODUCTION_DOMAIN``,
@ -132,22 +133,22 @@ class SubdomainMiddleware(object):
class SingleVersionMiddleware(object): class SingleVersionMiddleware(object):
"""Reset urlconf for requests for 'single_version' docs. """
Reset urlconf for requests for 'single_version' docs.
In settings.MIDDLEWARE_CLASSES, SingleVersionMiddleware must follow
after SubdomainMiddleware.
In settings.MIDDLEWARE_CLASSES, SingleVersionMiddleware must follow after
SubdomainMiddleware.
""" """
def _get_slug(self, request): 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 If URL is like '/docs/<project_name>/', we split path
and pull out slug. and pull out slug.
If URL is subdomain or CNAME, we simply read request.slug, which is If URL is subdomain or CNAME, we simply read request.slug, which is
set by SubdomainMiddleware. set by SubdomainMiddleware.
""" """
slug = None slug = None
if hasattr(request, 'slug'): if hasattr(request, 'slug'):
@ -187,16 +188,16 @@ class SingleVersionMiddleware(object):
class ProxyMiddleware(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 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. causes each request's REMOTE_ADDR to be set to 127.0.0.1. Note that this
Note that this does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind a reverse proxy
a reverse proxy that sets HTTP_X_FORWARDED_FOR automatically, do not use that sets HTTP_X_FORWARDED_FOR automatically, do not use this middleware.
this middleware. Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and because this sets
because this sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means anybody can "fake"
anybody can "fake" their IP address. Only use this when you can absolutely their IP address. Only use this when you can absolutely trust the value of
trust the value of HTTP_X_FORWARDED_FOR. HTTP_X_FORWARDED_FOR.
""" """
def process_request(self, request): 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 __future__ import absolute_import
from builtins import object from builtins import object
@ -54,7 +54,7 @@ class ResolverBase(object):
def base_resolve_path(self, project_slug, filename, version_slug=None, def base_resolve_path(self, project_slug, filename, version_slug=None,
language=None, private=False, single_version=None, language=None, private=False, single_version=None,
subproject_slug=None, subdomain=None, cname=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 # Only support `/docs/project' URLs outside our normal environment. Normally
# the path should always have a subdomain or CNAME domain # the path should always have a subdomain or CNAME domain
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -80,7 +80,7 @@ class ResolverBase(object):
def resolve_path(self, project, filename='', version_slug=None, def resolve_path(self, project, filename='', version_slug=None,
language=None, single_version=None, subdomain=None, language=None, single_version=None, subdomain=None,
cname=None, private=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() relation = project.superprojects.first()
cname = cname or project.domains.filter(canonical=True).first() cname = cname or project.domains.filter(canonical=True).first()
main_language_project = project.main_language_project main_language_project = project.main_language_project
@ -145,7 +145,8 @@ class ResolverBase(object):
) )
def _get_canonical_project(self, project): 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 :type project: Project
:rtype: Project :rtype: Project
@ -159,7 +160,7 @@ class ResolverBase(object):
return project return project
def _get_project_subdomain(self, 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) public_domain = getattr(settings, 'PUBLIC_DOMAIN', None)
if self._use_subdomain(): if self._use_subdomain():
project = self._get_canonical_project(project) project = self._get_canonical_project(project)
@ -177,9 +178,10 @@ class ResolverBase(object):
def _fix_filename(self, project, filename): 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 # Bail out on non-html files
if '.' in filename and '.html' not in filename: if '.' in filename and '.html' not in filename:
@ -203,7 +205,7 @@ class ResolverBase(object):
return path return path
def _use_subdomain(self): 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) use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False)
public_domain = getattr(settings, 'PUBLIC_DOMAIN', None) public_domain = getattr(settings, 'PUBLIC_DOMAIN', None)
return use_subdomain and public_domain is not 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 __future__ import absolute_import
from builtins import object from builtins import object
@ -8,11 +8,12 @@ import sys
class Settings(object): class Settings(object):
"""Class-based settings wrapper""" """Class-based settings wrapper."""
@classmethod @classmethod
def load_settings(cls, module_name): 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 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 begin with ``_``. These members will be set as attributes on the module

View File

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

View File

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

View File

@ -1,4 +1,4 @@
"""Common utilty functions""" """Common utilty functions."""
from __future__ import absolute_import 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): 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 If project has a ``build_queue``, execute task on this build queue. Queue
will be prefixed with ``build-`` to unify build queue names. 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, def send_email(recipient, subject, template, template_html, context=None,
request=None, from_email=None, **kwargs): # pylint: disable=unused-argument 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:: .. seealso::
@ -152,7 +154,8 @@ def send_email(recipient, subject, template, template_html, context=None,
def slugify(value, *args, **kwargs): 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 :param dns_safe: Remove underscores from slug as well
""" """
@ -170,9 +173,9 @@ def safe_makedirs(directory_name):
""" """
Safely create a directory. Safely create a directory.
Makedirs has an issue where it has a race condition around Makedirs has an issue where it has a race condition around checking for a
checking for a directory and then creating it. directory and then creating it. This catches the exception in the case where
This catches the exception in the case where the dir already exists. the dir already exists.
""" """
try: try:
os.makedirs(directory_name) 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 from __future__ import absolute_import
import inspect import inspect
@ -9,7 +9,8 @@ import six
def get_override_class(proxy_class, default_class=None): 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 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 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): 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 def __getattr__(cls, attr): # noqa: pep8 false positive
proxy_class = get_override_class(cls, getattr(cls, '_default_class')) proxy_class = get_override_class(cls, getattr(cls, '_default_class'))
@ -42,7 +44,8 @@ class SettingsOverrideMeta(type):
class SettingsOverrideObject(six.with_metaclass(SettingsOverrideMeta, object)): 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 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 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 _override_setting = None
def __new__(cls, *args, **kwargs): 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 Create an instance of the underlying target class and return instead of
this class. 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. 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 = "" ret = ""
all_built = {} all_built = {}
@ -152,7 +153,7 @@ def _build_url(url, projects, branches):
@csrf_exempt @csrf_exempt
def github_build(request): # noqa: D205 def github_build(request): # noqa: D205
""" """
GitHub webhook consumer GitHub webhook consumer.
.. warning:: **DEPRECATED** .. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.GitHubWebhookView` Use :py:cls:`readthedocs.restapi.views.integrations.GitHubWebhookView`
@ -206,7 +207,8 @@ def github_build(request): # noqa: D205
@csrf_exempt @csrf_exempt
def gitlab_build(request): # noqa: D205 def gitlab_build(request): # noqa: D205
"""GitLab webhook consumer """
GitLab webhook consumer.
.. warning:: **DEPRECATED** .. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.GitLabWebhookView` Use :py:cls:`readthedocs.restapi.views.integrations.GitLabWebhookView`
@ -239,7 +241,8 @@ def gitlab_build(request): # noqa: D205
@csrf_exempt @csrf_exempt
def bitbucket_build(request): def bitbucket_build(request):
"""Consume webhooks from multiple versions of Bitbucket's API """
Consume webhooks from multiple versions of Bitbucket's API.
.. warning:: **DEPRECATED** .. warning:: **DEPRECATED**
Use :py:cls:`readthedocs.restapi.views.integrations.BitbucketWebhookView` Use :py:cls:`readthedocs.restapi.views.integrations.BitbucketWebhookView`
@ -307,11 +310,13 @@ def bitbucket_build(request):
@csrf_exempt @csrf_exempt
def generic_build(request, project_id_or_slug=None): def generic_build(request, project_id_or_slug=None):
"""Generic webhook build endpoint """
Generic webhook build endpoint.
.. warning:: **DEPRECATED** .. 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: try:
project = Project.objects.get(pk=project_id_or_slug) 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/ .. _MkDocs: http://www.mkdocs.org/
""" """
from __future__ import absolute_import from __future__ import absolute_import
import os import os
@ -22,10 +22,10 @@ OVERRIDE_TEMPLATE_DIR = '%s/readthedocs/templates/mkdocs/overrides' % settings.S
def get_absolute_media_url(): 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. Mkdocs needs a full domain because it tries to link to local media files.
""" """
media_url = settings.MEDIA_URL media_url = settings.MEDIA_URL
@ -38,7 +38,7 @@ def get_absolute_media_url():
class BaseMkdocs(BaseBuilder): class BaseMkdocs(BaseBuilder):
"""Mkdocs builder""" """Mkdocs builder."""
use_theme = True use_theme = True
@ -50,10 +50,10 @@ class BaseMkdocs(BaseBuilder):
self.root_path = self.version.project.checkout_path(self.version.slug) self.root_path = self.version.project.checkout_path(self.version.slug)
def load_yaml_config(self): def load_yaml_config(self):
"""Load a YAML config. """
Load a YAML config.
Raise BuildEnvironmentError if failed due to syntax errors. Raise BuildEnvironmentError if failed due to syntax errors.
""" """
try: try:
return yaml.safe_load( return yaml.safe_load(
@ -74,7 +74,7 @@ class BaseMkdocs(BaseBuilder):
note,)) note,))
def append_conf(self, **__): def append_conf(self, **__):
"""Set mkdocs config values""" """Set mkdocs config values."""
# Pull mkdocs config data # Pull mkdocs config data
user_config = self.load_yaml_config() user_config = self.load_yaml_config()

View File

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

View File

@ -76,7 +76,8 @@ class PythonEnvironment(object):
) )
def venv_bin(self, filename=None): 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 :param filename: If specified, add this filename to the path return
:returns: Path to virtualenv bin or filename in virtualenv bin :returns: Path to virtualenv bin or filename in virtualenv bin
@ -89,10 +90,10 @@ class PythonEnvironment(object):
class Virtualenv(PythonEnvironment): class Virtualenv(PythonEnvironment):
"""A virtualenv_ environment. """
A virtualenv_ environment.
.. _virtualenv: https://virtualenv.pypa.io/ .. _virtualenv: https://virtualenv.pypa.io/
""" """
def venv_path(self): def venv_path(self):
@ -203,10 +204,10 @@ class Virtualenv(PythonEnvironment):
class Conda(PythonEnvironment): class Conda(PythonEnvironment):
"""A Conda_ environment. """
A Conda_ environment.
.. _Conda: https://conda.io/docs/ .. _Conda: https://conda.io/docs/
""" """
def venv_path(self): 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. Gold Membership is Read the Docs' program for recurring, monthly donations.
""" """
default_app_config = 'readthedocs.gold.apps.GoldAppConfig' 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 __future__ import absolute_import
from builtins import object from builtins import object
@ -12,7 +12,8 @@ from .models import LEVEL_CHOICES, GoldUser
class GoldSubscriptionForm(StripeResourceMixin, StripeModelForm): class GoldSubscriptionForm(StripeResourceMixin, StripeModelForm):
"""Gold subscription payment form """
Gold subscription payment form.
This extends the common base form for handling Stripe subscriptions. Credit This extends the common base form for handling Stripe subscriptions. Credit
card fields for card number, expiry, and CVV are extended from 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 __future__ import absolute_import
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
@ -22,7 +22,7 @@ from .models import GoldUser
class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin): class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin):
"""Gold subscription mixin for view classes""" """Gold subscription mixin for view classes."""
model = GoldUser model = GoldUser
form_class = GoldSubscriptionForm form_class = GoldSubscriptionForm
@ -34,7 +34,7 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin
return None return None
def get_form(self, data=None, files=None, **kwargs): 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 kwargs['customer'] = self.request.user
return super(GoldSubscriptionMixin, self).get_form(data, files, **kwargs) return super(GoldSubscriptionMixin, self).get_form(data, files, **kwargs)
@ -57,7 +57,8 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin
class DetailGoldSubscription(GoldSubscriptionMixin, DetailView): class DetailGoldSubscription(GoldSubscriptionMixin, DetailView):
def get(self, request, *args, **kwargs): 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 If there is a gold subscription instance, then we show the normal detail
page, otherwise show the registration form page, otherwise show the registration form
@ -74,7 +75,8 @@ class UpdateGoldSubscription(GoldSubscriptionMixin, UpdateView):
class DeleteGoldSubscription(GoldSubscriptionMixin, DeleteView): class DeleteGoldSubscription(GoldSubscriptionMixin, DeleteView):
"""Delete Gold subscription view """
Delete Gold subscription view.
On object deletion, the corresponding Stripe customer is deleted as well. On object deletion, the corresponding Stripe customer is deleted as well.
Deletion is triggered on subscription deletion using a signal, ensuring the 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') success_message = _('Your subscription has been cancelled')
def post(self, request, *args, **kwargs): 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) resp = super(DeleteGoldSubscription, self).post(request, *args, **kwargs)
success_message = self.get_success_message({}) success_message = self.get_success_message({})
if success_message: if success_message:

View File

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

View File

@ -1,8 +1,9 @@
"""Integration utility functions""" """Integration utility functions."""
def normalize_request_payload(request): 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. 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 Notifications are important communications to users that need to be as visible
as possible. We support different backends to make notifications visible in as possible. We support different backends to make notifications visible in
@ -10,7 +11,6 @@ on the site.
.. _`django-messages-extends`: https://github.com .. _`django-messages-extends`: https://github.com
/AliLozano/django-messages-extends/ /AliLozano/django-messages-extends/
""" """
from .notification import Notification from .notification import Notification
from .backends import send_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 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 Django settings. For example, they might be e-mailed to users as well as
displayed on the site. displayed on the site.
""" """
from __future__ import absolute_import from __future__ import absolute_import
@ -19,7 +19,8 @@ from .constants import LEVEL_MAPPING, REQUIREMENT, HTML
def send_notification(request, notification): 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 Backends should be listed in the settings ``NOTIFICATION_BACKENDS``, which
should be a list of class paths to be loaded, using the standard Django should be a list of class paths to be loaded, using the standard Django
@ -42,7 +43,8 @@ class Backend(object):
class EmailBackend(Backend): 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 The content body is first rendered from an on-disk template, then passed
into the standard email templates as a string. into the standard email templates as a string.
@ -66,7 +68,8 @@ class EmailBackend(Backend):
class SiteBackend(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` This uses persistent messageing levels provided by :py:mod:`message_extends`
and stores persistent messages in the database. 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): class SendNotificationForm(forms.Form):
"""Send notification form """
Send notification form.
Used for sending a notification to a list of users from admin pages 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] in self.notification_classes]
def clean_source(self): 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'] source = self.cleaned_data['source']
classes = dict((cls.name, cls) for cls in self.notification_classes) classes = dict((cls.name, cls) for cls in self.notification_classes)
return classes.get(source, None) return classes.get(source, None)

View File

@ -13,14 +13,14 @@ from . import constants
class Notification(object): 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 This class provides an interface to construct notification messages by
rendering Django templates. The ``Notification`` itself is not expected rendering Django templates. The ``Notification`` itself is not expected
to be persisted by the backends. to be persisted by the backends.
Call .send() to send the notification. Call .send() to send the notification.
""" """
name = None name = None
@ -75,7 +75,8 @@ class Notification(object):
) )
def send(self): 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, In order to limit which backends a notification will send out from,
override this method and duplicate the logic 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): 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 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. 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): 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: Accepts the following additional parameters:
@ -28,7 +29,8 @@ class SendNotificationView(FormView):
notification_classes = [] notification_classes = []
def get_form_kwargs(self): 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 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. form post variables. Drop additional fields if we see the send button.
@ -41,14 +43,14 @@ class SendNotificationView(FormView):
return kwargs return kwargs
def get_initial(self): 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 = super(SendNotificationView, self).get_initial()
initial['_selected_action'] = self.request.POST.getlist( initial['_selected_action'] = self.request.POST.getlist(
admin.ACTION_CHECKBOX_NAME) admin.ACTION_CHECKBOX_NAME)
return initial return initial
def form_valid(self, form): def form_valid(self, form):
"""If form is valid, send notification to recipients""" """If form is valid, send notification to recipients."""
count = 0 count = 0
notification_cls = form.cleaned_data['source'] notification_cls = form.cleaned_data['source']
for obj in self.get_queryset().all(): for obj in self.get_queryset().all():
@ -65,7 +67,8 @@ class SendNotificationView(FormView):
return HttpResponseRedirect(self.request.get_full_path()) return HttpResponseRedirect(self.request.get_full_path())
def get_object_recipients(self, obj): 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 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 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') return self.kwargs.get('queryset')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Return queryset in context""" """Return queryset in context."""
context = super(SendNotificationView, self).get_context_data(**kwargs) context = super(SendNotificationView, self).get_context_data(**kwargs)
recipients = [] recipients = []
for obj in self.get_queryset().all(): for obj in self.get_queryset().all():
@ -96,7 +99,8 @@ class SendNotificationView(FormView):
def message_user(self, message, level=messages.INFO, extra_tags='', def message_user(self, message, level=messages.INFO, extra_tags='',
fail_silently=False): 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 Send message through messages framework
""" """

View File

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

View File

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

View File

@ -1,4 +1,4 @@
"""Payment forms""" """Payment forms."""
from __future__ import absolute_import from __future__ import absolute_import
from builtins import str from builtins import str
@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
class StripeResourceMixin(object): 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): def ensure_stripe_resource(self, resource, attrs):
try: try:
@ -59,7 +59,8 @@ class StripeResourceMixin(object):
class StripeModelForm(forms.ModelForm): 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 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. 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) super(StripeModelForm, self).__init__(*args, **kwargs)
def validate_stripe(self): 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 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 to actually create the objects, as that is what will provide us with
@ -133,12 +135,12 @@ class StripeModelForm(forms.ModelForm):
return data return data
def clean(self): 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 This will handle ensuring a customer and subscription exist and will
raise any issues as validation errors. This is required because part raise any issues as validation errors. This is required because part of
of Stripe's validation happens on the API call to establish a Stripe's validation happens on the API call to establish a subscription.
subscription.
""" """
cleaned_data = super(StripeModelForm, self).clean() cleaned_data = super(StripeModelForm, self).clean()
@ -171,7 +173,8 @@ class StripeModelForm(forms.ModelForm):
return cleaned_data return cleaned_data
def clear_card_data(self): 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 This requires the form was created by passing in a mutable QueryDict
instance, see :py:class:`readthedocs.payments.mixin.StripeMixin` 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 __future__ import absolute_import
from builtins import object from builtins import object
@ -7,7 +7,7 @@ from django.conf import settings
class StripeMixin(object): 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): def get_context_data(self, **kwargs):
context = super(StripeMixin, self).get_context_data(**kwargs) context = super(StripeMixin, self).get_context_data(**kwargs)
@ -15,7 +15,8 @@ class StripeMixin(object):
return context return context
def get_form(self, data=None, files=None, **kwargs): 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 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 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 These are mostly one-off functions. Define the bulk of Stripe operations on
:py:class:`readthedocs.payments.forms.StripeResourceMixin`. :py:class:`readthedocs.payments.forms.StripeResourceMixin`.
@ -12,7 +13,7 @@ stripe.api_key = getattr(settings, 'STRIPE_SECRET', None)
def delete_customer(customer_id): def delete_customer(customer_id):
"""Delete customer from Stripe, cancelling subscriptions""" """Delete customer from Stripe, cancelling subscriptions."""
try: try:
customer = stripe.Customer.retrieve(customer_id) customer = stripe.Customer.retrieve(customer_id)
return customer.delete() return customer.delete()
@ -21,7 +22,7 @@ def delete_customer(customer_id):
def cancel_subscription(customer_id, subscription_id): def cancel_subscription(customer_id, subscription_id):
"""Cancel Stripe subscription, if it exists""" """Cancel Stripe subscription, if it exists."""
try: try:
customer = stripe.Customer.retrieve(customer_id) customer = stripe.Customer.retrieve(customer_id)
if hasattr(customer, 'subscriptions'): if hasattr(customer, 'subscriptions'):

View File

@ -74,7 +74,8 @@ class DomainInline(admin.TabularInline):
class ProjectOwnerBannedFilter(admin.SimpleListFilter): 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` There are problems adding `users__profile__banned` to the `list_filter`
attribute, so we'll create a basic filter to capture banned owners. attribute, so we'll create a basic filter to capture banned owners.
@ -98,7 +99,7 @@ class ProjectOwnerBannedFilter(admin.SimpleListFilter):
class ProjectAdmin(GuardedModelAdmin): class ProjectAdmin(GuardedModelAdmin):
"""Project model admin view""" """Project model admin view."""
prepopulated_fields = {'slug': ('name',)} prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'repo', 'repo_type', 'allow_comments', 'featured', 'theme') 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' send_owner_email.short_description = 'Notify project owners'
def ban_owner(self, request, queryset): 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 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 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' ban_owner.short_description = 'Ban project owner'
def delete_selected_and_artifacts(self, request, queryset): 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 Prior to the query delete, broadcast tasks to delete HTML artifacts from
application instances. application instances.

View File

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

View File

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

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Project search indexes Project search indexes.
.. deprecated:: .. 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 from __future__ import absolute_import
@ -26,7 +27,7 @@ log = logging.getLogger(__name__)
class ProjectIndex(indexes.SearchIndex, indexes.Indexable): class ProjectIndex(indexes.SearchIndex, indexes.Indexable):
"""Project search index""" """Project search index."""
text = CharField(document=True, use_template=True) text = CharField(document=True, use_template=True)
author = CharField() author = CharField()
@ -45,14 +46,14 @@ class ProjectIndex(indexes.SearchIndex, indexes.Indexable):
return Project return Project
def index_queryset(self, using=None): 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() return self.get_model().objects.public()
# TODO Should prob make a common subclass for this and FileIndex # TODO Should prob make a common subclass for this and FileIndex
class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable): class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable):
"""Search index for imported files""" """Search index for imported files."""
text = CharField(document=True) text = CharField(document=True)
author = CharField() author = CharField()
@ -71,7 +72,8 @@ class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable):
return obj.get_absolute_url() return obj.get_absolute_url()
def prepare_text(self, obj): 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 This only works on machines that have the html files for the projects
checked out. checked out.
@ -107,6 +109,6 @@ class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable):
return ImportedFile return ImportedFile
def index_queryset(self, using=None): 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 return (self.get_model().objects
.filter(project__privacy_level=constants.PUBLIC)) .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 This includes fetching repository code, cleaning ``conf.py`` files, and
rebuilding documentation. rebuilding documentation.
@ -186,7 +187,8 @@ class UpdateDocsTask(Task):
return True return True
def run_setup(self, record=True): def run_setup(self, record=True):
"""Run setup in the local environment. """
Run setup in the local environment.
Return True if successful. Return True if successful.
""" """
@ -236,11 +238,11 @@ class UpdateDocsTask(Task):
return True return True
def run_build(self, docker=False, record=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 If `docker` is True, or Docker is enabled by the settings.DOCKER_ENABLE
setting, then build in a Docker environment. Otherwise build locally. setting, then build in a Docker environment. Otherwise build locally.
""" """
env_vars = self.get_env_vars() env_vars = self.get_env_vars()
@ -294,13 +296,13 @@ class UpdateDocsTask(Task):
@staticmethod @staticmethod
def get_project(project_pk): def get_project(project_pk):
"""Get project from API""" """Get project from API."""
project_data = api_v2.project(project_pk).get() project_data = api_v2.project(project_pk).get()
return APIProject(**project_data) return APIProject(**project_data)
@staticmethod @staticmethod
def get_version(project, version_pk): def get_version(project, version_pk):
"""Ensure we're using a sane version""" """Ensure we're using a sane version."""
if version_pk: if version_pk:
version_data = api_v2.version(version_pk).get() version_data = api_v2.version(version_pk).get()
else: else:
@ -312,7 +314,7 @@ class UpdateDocsTask(Task):
@staticmethod @staticmethod
def get_build(build_pk): def get_build(build_pk):
""" """
Retrieve build object from API Retrieve build object from API.
:param build_pk: Build primary key :param build_pk: Build primary key
""" """
@ -369,7 +371,7 @@ class UpdateDocsTask(Task):
def update_documentation_type(self): 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 This used to determine the type and automatically set the documentation
type to Sphinx for rST and Mkdocs for markdown. It now just forces 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, def update_app_instances(self, html=False, localmedia=False, search=False,
pdf=False, epub=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, This triggers updates across application instances for html, pdf, epub,
downloads, and search. Tasks are broadcast to all web servers from here. downloads, and search. Tasks are broadcast to all web servers from here.
@ -439,7 +442,8 @@ class UpdateDocsTask(Task):
self.python_env.install_package() self.python_env.install_package()
def build_docs(self): 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 Executes the necessary builds for this task and returns whether the
build was successful or not. build was successful or not.
@ -465,7 +469,7 @@ class UpdateDocsTask(Task):
return outcomes return outcomes
def build_docs_html(self): def build_docs_html(self):
"""Build HTML docs""" """Build HTML docs."""
html_builder = get_builder_class(self.project.documentation_type)( html_builder = get_builder_class(self.project.documentation_type)(
build_env=self.build_env, build_env=self.build_env,
python_env=self.python_env, python_env=self.python_env,
@ -489,7 +493,7 @@ class UpdateDocsTask(Task):
return success return success
def build_docs_search(self): def build_docs_search(self):
"""Build search data with separate build""" """Build search data with separate build."""
if self.build_search: if self.build_search:
if self.project.is_type_mkdocs: if self.project.is_type_mkdocs:
return self.build_docs_class('mkdocs_json') return self.build_docs_class('mkdocs_json')
@ -498,7 +502,7 @@ class UpdateDocsTask(Task):
return False return False
def build_docs_localmedia(self): 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: if 'htmlzip' not in self.config.formats:
return False return False
@ -508,7 +512,7 @@ class UpdateDocsTask(Task):
return False return False
def build_docs_pdf(self): def build_docs_pdf(self):
"""Build PDF docs""" """Build PDF docs."""
if ('pdf' not in self.config.formats or if ('pdf' not in self.config.formats or
self.project.slug in HTML_ONLY or self.project.slug in HTML_ONLY or
not self.project.is_type_sphinx): not self.project.is_type_sphinx):
@ -516,7 +520,7 @@ class UpdateDocsTask(Task):
return self.build_docs_class('sphinx_pdf') return self.build_docs_class('sphinx_pdf')
def build_docs_epub(self): def build_docs_epub(self):
"""Build ePub docs""" """Build ePub docs."""
if ('epub' not in self.config.formats or if ('epub' not in self.config.formats or
self.project.slug in HTML_ONLY or self.project.slug in HTML_ONLY or
not self.project.is_type_sphinx): not self.project.is_type_sphinx):
@ -524,7 +528,8 @@ class UpdateDocsTask(Task):
return self.build_docs_class('sphinx_epub') return self.build_docs_class('sphinx_epub')
def build_docs_class(self, builder_class): 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 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 only raise a warning exception here. A hard error will halt the build
@ -536,14 +541,14 @@ class UpdateDocsTask(Task):
return success return success
def send_notifications(self): 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']) send_notifications.delay(self.version.pk, build_pk=self.build['id'])
@app.task() @app.task()
def update_imported_docs(version_pk): 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 :param version_pk: Version id to update
""" """
@ -626,10 +631,11 @@ def update_imported_docs(version_pk):
@app.task(queue='web') @app.task(queue='web')
def sync_files(project_pk, version_pk, hostname=None, html=False, def sync_files(project_pk, version_pk, hostname=None, html=False,
localmedia=False, search=False, pdf=False, epub=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 This task broadcasts from a build instance on build completion and performs
performs synchronization of build artifacts on each application instance. synchronization of build artifacts on each application instance.
""" """
# Clean up unused artifacts # Clean up unused artifacts
if not pdf: if not pdf:
@ -658,7 +664,8 @@ def sync_files(project_pk, version_pk, hostname=None, html=False,
@app.task(queue='web') @app.task(queue='web')
def move_files(version_pk, hostname, html=False, localmedia=False, search=False, def move_files(version_pk, hostname, html=False, localmedia=False, search=False,
pdf=False, epub=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 version_pk: Version id to sync files for
:param hostname: Hostname to sync to :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') @app.task(queue='web')
def update_search(version_pk, commit, delete_non_commit_files=True): 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 version_pk: Version id to update
:param commit: Commit that updated index :param commit: Commit that updated index
@ -814,7 +822,8 @@ def fileify(version_pk, commit):
def _manage_imported_files(version, path, commit): def _manage_imported_files(version, path, commit):
"""Update imported files for version """
Update imported files for version.
:param version: Version instance :param version: Version instance
:param path: Path to search :param path: Path to search
@ -869,7 +878,8 @@ def send_notifications(version_pk, build_pk):
def email_notification(version, build, email): 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 version: :py:class:`Version` instance that failed
:param build: :py:class:`Build` 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): 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 version: Version instance to send hook for
:param build: Build instance that failed :param build: Build instance that failed
@ -928,7 +939,8 @@ def webhook_notification(version, build, hook_url):
@app.task(queue='web') @app.task(queue='web')
def update_static_metadata(project_pk, path=None): 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: Metadata settings include the following project settings:
@ -979,8 +991,8 @@ def remove_dir(path):
""" """
Remove a directory on the build/celery server. Remove a directory on the build/celery server.
This is mainly a wrapper around shutil.rmtree so that app servers This is mainly a wrapper around shutil.rmtree so that app servers can kill
can kill things on the build server. things on the build server.
""" """
log.info("Removing %s", path) log.info("Removing %s", path)
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)
@ -988,7 +1000,7 @@ def remove_dir(path):
@app.task() @app.task()
def clear_artifacts(version_pk): def clear_artifacts(version_pk):
"""Remove artifacts from the web servers""" """Remove artifacts from the web servers."""
version = Version.objects.get(pk=version_pk) version = Version.objects.get(pk=version_pk)
clear_pdf_artifacts(version) clear_pdf_artifacts(version)
clear_epub_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 from __future__ import absolute_import
@ -32,7 +32,8 @@ def version_from_slug(slug, version):
def find_file(filename): 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 :param file: Filename to match
:returns: A list of matching filenames. :returns: A list of matching filenames.
@ -45,7 +46,8 @@ def find_file(filename):
def run(*commands): 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 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 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): 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 Write ``contents`` to the given ``filename``. If the filename's
directory does not exist, it is created. Contents are written as UTF-8, 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 __future__ import absolute_import
from builtins import object from builtins import object
@ -9,7 +9,8 @@ from readthedocs.projects.models import Project
class ProjectRelationMixin(object): 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 This mixin class is used for model views on models that have a relation
to the :py:cls:`Project` model. 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 from __future__ import absolute_import
import logging import logging
@ -52,7 +52,7 @@ class PrivateViewMixin(LoginRequiredMixin):
class ProjectDashboard(PrivateViewMixin, ListView): class ProjectDashboard(PrivateViewMixin, ListView):
"""Project dashboard""" """Project dashboard."""
model = Project model = Project
template_name = 'projects/project_dashboard.html' template_name = 'projects/project_dashboard.html'
@ -75,7 +75,8 @@ class ProjectDashboard(PrivateViewMixin, ListView):
@login_required @login_required
def project_manage(__, project_slug): def project_manage(__, project_slug):
"""Project management view """
Project management view.
Where you will have links to edit the projects' configuration, edit the Where you will have links to edit the projects' configuration, edit the
files associated with that project, etc. files associated with that project, etc.
@ -131,7 +132,8 @@ class ProjectAdvancedUpdate(ProjectSpamMixin, PrivateViewMixin, UpdateView):
@login_required @login_required
def project_versions(request, project_slug): 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 Shows the available versions and lets the user choose which ones he would
like to have built. like to have built.
@ -161,7 +163,7 @@ def project_versions(request, project_slug):
@login_required @login_required
def project_version_detail(request, project_slug, version_slug): 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) project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
version = get_object_or_404( version = get_object_or_404(
Version.objects.public(user=request.user, project=project, only_active=False), Version.objects.public(user=request.user, project=project, only_active=False),
@ -189,7 +191,8 @@ def project_version_detail(request, project_slug, version_slug):
@login_required @login_required
def project_delete(request, project_slug): 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 Make a project as deleted on POST, otherwise show a form asking for
confirmation of delete. confirmation of delete.
@ -213,14 +216,14 @@ def project_delete(request, project_slug):
class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView): class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
"""Project import wizard""" """Project import wizard."""
form_list = [('basics', ProjectBasicsForm), form_list = [('basics', ProjectBasicsForm),
('extra', ProjectExtraForm)] ('extra', ProjectExtraForm)]
condition_dict = {'extra': lambda self: self.is_advanced()} condition_dict = {'extra': lambda self: self.is_advanced()}
def get_form_kwargs(self, step=None): def get_form_kwargs(self, step=None):
"""Get args to pass into form instantiation""" """Get args to pass into form instantiation."""
kwargs = {} kwargs = {}
kwargs['user'] = self.request.user kwargs['user'] = self.request.user
if step == 'basics': if step == 'basics':
@ -228,11 +231,12 @@ class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
return kwargs return kwargs
def get_template_names(self): 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) return 'projects/import_{0}.html'.format(self.steps.current)
def done(self, form_list, **kwargs): 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 Don't save form data directly, instead bypass documentation building and
other side effects for now, by signalling a save without commit. Then, other side effects for now, by signalling a save without commit. Then,
@ -260,14 +264,14 @@ class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView):
args=[project.slug])) args=[project.slug]))
def is_advanced(self): 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 {} data = self.get_cleaned_data_for_step('basics') or {}
return data.get('advanced', True) return data.get('advanced', True)
class ImportDemoView(PrivateViewMixin, View): 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 form_class = ProjectBasicsForm
request = None request = None
@ -275,7 +279,7 @@ class ImportDemoView(PrivateViewMixin, View):
kwargs = None kwargs = None
def get(self, request, *args, **kwargs): 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.request = request
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
@ -305,7 +309,7 @@ class ImportDemoView(PrivateViewMixin, View):
args=[project.slug])) args=[project.slug]))
def get_form_data(self): def get_form_data(self):
"""Get form data to post to import form""" """Get form data to post to import form."""
return { return {
'name': '{0}-demo'.format(self.request.user.username), 'name': '{0}-demo'.format(self.request.user.username),
'repo_type': 'git', 'repo_type': 'git',
@ -313,13 +317,14 @@ class ImportDemoView(PrivateViewMixin, View):
} }
def get_form_kwargs(self): def get_form_kwargs(self):
"""Form kwargs passed in during instantiation""" """Form kwargs passed in during instantiation."""
return {'user': self.request.user} return {'user': self.request.user}
class ImportView(PrivateViewMixin, TemplateView): 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 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 :py:class:`ImportWizardView`. The import templates will redirect the form to
@ -330,7 +335,8 @@ class ImportView(PrivateViewMixin, TemplateView):
wizard_class = ImportWizardView wizard_class = ImportWizardView
def get(self, request, *args, **kwargs): 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 Adds a warning to the listing if any of the accounts connected for the
user are not supported accounts. user are not supported accounts.
@ -377,7 +383,7 @@ class ImportView(PrivateViewMixin, TemplateView):
@login_required @login_required
def edit_alias(request, project_slug, alias_id=None): 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) proj = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
if alias_id: if alias_id:
alias = proj.aliases.get(pk=alias_id) alias = proj.aliases.get(pk=alias_id)
@ -455,7 +461,7 @@ class ProjectRelationshipDelete(ProjectRelationshipMixin, DeleteView):
@login_required @login_required
def project_users(request, project_slug): 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), project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug) slug=project_slug)
@ -490,7 +496,7 @@ def project_users_delete(request, project_slug):
@login_required @login_required
def project_notifications(request, project_slug): 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), project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug) slug=project_slug)
@ -538,7 +544,7 @@ def project_comments_settings(request, project_slug):
@login_required @login_required
def project_notifications_delete(request, project_slug): def project_notifications_delete(request, project_slug):
"""Project notifications delete confirmation view""" """Project notifications delete confirmation view."""
if request.method != 'POST': if request.method != 'POST':
return HttpResponseNotAllowed('Only POST is allowed') return HttpResponseNotAllowed('Only POST is allowed')
project = get_object_or_404(Project.objects.for_admin_user(request.user), 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 @login_required
def project_translations(request, project_slug): 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), project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug) slug=project_slug)
form = TranslationForm(data=request.POST or None, parent=project) form = TranslationForm(data=request.POST or None, parent=project)
@ -587,7 +593,7 @@ def project_translations_delete(request, project_slug, child_slug):
@login_required @login_required
def project_redirects(request, project_slug): 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), project = get_object_or_404(Project.objects.for_admin_user(request.user),
slug=project_slug) slug=project_slug)
@ -609,7 +615,7 @@ def project_redirects(request, project_slug):
@login_required @login_required
def project_redirects_delete(request, project_slug): def project_redirects_delete(request, project_slug):
"""Project redirect delete view""" """Project redirect delete view."""
if request.method != 'POST': if request.method != 'POST':
return HttpResponseNotAllowed('Only POST is allowed') return HttpResponseNotAllowed('Only POST is allowed')
project = get_object_or_404(Project.objects.for_admin_user(request.user), 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 @login_required
def project_version_delete_html(request, project_slug, version_slug): 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 This marks a version as not built
""" """
@ -672,7 +679,7 @@ class DomainDelete(DomainMixin, DeleteView):
class IntegrationMixin(ProjectAdminMixin, PrivateViewMixin): class IntegrationMixin(ProjectAdminMixin, PrivateViewMixin):
"""Project external service mixin for listing webhook objects""" """Project external service mixin for listing webhook objects."""
model = Integration model = Integration
integration_url_field = 'integration_pk' integration_url_field = 'integration_pk'
@ -689,7 +696,7 @@ class IntegrationMixin(ProjectAdminMixin, PrivateViewMixin):
return self.model.objects.filter(project=self.project) return self.model.objects.filter(project=self.project)
def get_integration(self): 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: if self.integration_url_field not in self.kwargs:
return None return None
return get_object_or_404( return get_object_or_404(
@ -765,7 +772,8 @@ class IntegrationExchangeDetail(IntegrationMixin, DetailView):
class IntegrationWebhookSync(IntegrationMixin, GenericView): class IntegrationWebhookSync(IntegrationMixin, GenericView):
"""Resync a project webhook """
Resync a project webhook.
The signal will add a success/failure message on the request. 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 __future__ import absolute_import
from collections import OrderedDict from collections import OrderedDict
@ -38,7 +38,7 @@ mimetypes.add_type("application/epub+zip", ".epub")
class ProjectIndex(ListView): class ProjectIndex(ListView):
"""List view of public :py:class:`Project` instances""" """List view of public :py:class:`Project` instances."""
model = Project model = Project
@ -70,7 +70,7 @@ project_index = ProjectIndex.as_view()
class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView): class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):
"""Display project onboard steps""" """Display project onboard steps."""
model = Project model = Project
slug_url_kwarg = 'project_slug' slug_url_kwarg = 'project_slug'
@ -106,7 +106,7 @@ class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView):
@never_cache @never_cache
def project_badge(request, project_slug): 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" badge_path = "projects/badges/%s.svg"
version_slug = request.GET.get('version', LATEST) version_slug = request.GET.get('version', LATEST)
try: try:
@ -128,7 +128,7 @@ def project_badge(request, project_slug):
def project_downloads(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) project = get_object_or_404(Project.objects.protected(request.user), slug=project_slug)
versions = Version.objects.public(user=request.user, project=project) versions = Version.objects.public(user=request.user, project=project)
version_data = OrderedDict() 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. .. warning:: This is linked directly from the HTML pages.
It should only care about the Version permissions, It should only care about the Version permissions,
not the actual Project permissions. not the actual Project permissions.
""" """
version = get_object_or_404( version = get_object_or_404(
Version.objects.public(user=request.user), Version.objects.public(user=request.user),
@ -189,7 +188,7 @@ def project_download_media(request, project_slug, type_, version_slug):
def search_autocomplete(request): def search_autocomplete(request):
"""Return a json list of project names""" """Return a json list of project names."""
if 'term' in request.GET: if 'term' in request.GET:
term = request.GET['term'] term = request.GET['term']
else: else:
@ -208,7 +207,7 @@ def search_autocomplete(request):
def version_autocomplete(request, project_slug): 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) queryset = Project.objects.public(request.user)
get_object_or_404(queryset, slug=project_slug) get_object_or_404(queryset, slug=project_slug)
versions = Version.objects.public(request.user) versions = Version.objects.public(request.user)
@ -247,7 +246,7 @@ def version_filter_autocomplete(request, project_slug):
def file_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: if 'term' in request.GET:
term = request.GET['term'] term = request.GET['term']
else: else:
@ -266,7 +265,7 @@ def file_autocomplete(request, project_slug):
def elastic_project_search(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) queryset = Project.objects.protected(request.user)
project = get_object_or_404(queryset, slug=project_slug) project = get_object_or_404(queryset, slug=project_slug)
version_slug = request.GET.get('version', LATEST) version_slug = request.GET.get('version', LATEST)
@ -340,7 +339,8 @@ def elastic_project_search(request, project_slug):
def project_versions(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. 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): 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), project = get_object_or_404(Project.objects.protected(request.user),
slug=project_slug) slug=project_slug)
analytics_cache = cache.get('analytics:%s' % 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): 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), project = get_object_or_404(Project.objects.protected(request.user),
slug=project_slug) slug=project_slug)
version = project.versions.get(slug=LATEST) 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 This module allows for parsing a URL path, looking up redirects associated
with it in the database, and generating a redirect response. with it in the database, and generating a redirect response.
These are not used directly as views; they are instead included into 404 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. handlers, so that redirects only take effect if no other view matches.
""" """
from __future__ import absolute_import from __future__ import absolute_import
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
@ -20,7 +20,8 @@ log = logging.getLogger(__name__)
def project_and_path_from_request(request, path): 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 Return a tuple (project, path) where `project` is a projects.Project if
a matching project exists, and `path` is the unmatched remainder of the 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` If the path does not match, or no matching project is found, then `project`
will be ``None``. will be ``None``.
""" """
if hasattr(request, 'slug'): if hasattr(request, 'slug'):
project_slug = request.slug project_slug = request.slug

View File

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

View File

@ -28,7 +28,8 @@ class ProjectSerializer(serializers.ModelSerializer):
class ProjectAdminSerializer(ProjectSerializer): 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 Includes special internal fields that don't need to be exposed through the
general API, mostly for fields used in the build process general API, mostly for fields used in the build process
@ -77,7 +78,7 @@ class VersionSerializer(serializers.ModelSerializer):
class VersionAdminSerializer(VersionSerializer): class VersionAdminSerializer(VersionSerializer):
"""Version serializer that returns admin project data""" """Version serializer that returns admin project data."""
project = ProjectAdminSerializer() project = ProjectAdminSerializer()
@ -93,7 +94,7 @@ class BuildCommandSerializer(serializers.ModelSerializer):
class BuildSerializer(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) commands = BuildCommandSerializer(many=True, read_only=True)
state_display = serializers.ReadOnlyField(source='get_state_display') state_display = serializers.ReadOnlyField(source='get_state_display')
@ -105,7 +106,7 @@ class BuildSerializer(serializers.ModelSerializer):
class BuildAdminSerializer(BuildSerializer): 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): class Meta(BuildSerializer.Meta):
exclude = () exclude = ()
@ -142,7 +143,7 @@ class RemoteOrganizationSerializer(serializers.ModelSerializer):
class RemoteRepositorySerializer(serializers.ModelSerializer): class RemoteRepositorySerializer(serializers.ModelSerializer):
"""Remote service repository serializer""" """Remote service repository serializer."""
organization = RemoteOrganizationSerializer() organization = RemoteOrganizationSerializer()
matches = serializers.SerializerMethodField() 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, def index_search_request(version, page_list, commit, project_scale, page_scale,
section=True, delete=True): 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 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. updated using the parent project's slug as the routing value.

View File

@ -39,7 +39,7 @@ class WebhookMixin(object):
integration_type = None integration_type = None
def post(self, request, project_slug): 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.request = request
self.project = None self.project = None
try: try:
@ -57,7 +57,7 @@ class WebhookMixin(object):
return Project.objects.get(**kwargs) return Project.objects.get(**kwargs)
def finalize_response(self, req, *args, **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) resp = super(WebhookMixin, self).finalize_response(req, *args, **kwargs)
if hasattr(self, 'project') and self.project: if hasattr(self, 'project') and self.project:
HttpExchange.objects.from_exchange( HttpExchange.objects.from_exchange(
@ -69,15 +69,16 @@ class WebhookMixin(object):
return resp return resp
def get_data(self): def get_data(self):
"""Normalize posted data""" """Normalize posted data."""
return normalize_request_payload(self.request) return normalize_request_payload(self.request)
def handle_webhook(self): def handle_webhook(self):
"""Handle webhook payload""" """Handle webhook payload."""
raise NotImplementedError raise NotImplementedError
def get_integration(self): 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 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 that a webhook has ever been created on our side. Most providers don't
@ -97,7 +98,8 @@ class WebhookMixin(object):
return integration return integration
def get_response_push(self, project, branches): 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:: Return a JSON response with the following::
@ -124,7 +126,8 @@ class WebhookMixin(object):
class GitHubWebhookView(WebhookMixin, APIView): class GitHubWebhookView(WebhookMixin, APIView):
"""Webhook consumer for GitHub """
Webhook consumer for GitHub.
Accepts webhook events from GitHub, 'push' events trigger builds. Expects the Accepts webhook events from GitHub, 'push' events trigger builds. Expects the
webhook event type will be included in HTTP header ``X-GitHub-Event``, and 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): class GitLabWebhookView(WebhookMixin, APIView):
"""Webhook consumer for GitLab """
Webhook consumer for GitLab.
Accepts webhook events from GitLab, 'push' events trigger builds. Accepts webhook events from GitLab, 'push' events trigger builds.
@ -195,7 +199,8 @@ class GitLabWebhookView(WebhookMixin, APIView):
class BitbucketWebhookView(WebhookMixin, APIView): class BitbucketWebhookView(WebhookMixin, APIView):
"""Webhook consumer for Bitbucket """
Webhook consumer for Bitbucket.
Accepts webhook events from Bitbucket, 'repo:push' events trigger builds. Accepts webhook events from Bitbucket, 'repo:push' events trigger builds.
@ -236,7 +241,8 @@ class BitbucketWebhookView(WebhookMixin, APIView):
class IsAuthenticatedOrHasToken(permissions.IsAuthenticated): 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 This does not check for instance-level permissions, as the check uses
methods from the view to determine if the token matches. methods from the view to determine if the token matches.
@ -250,7 +256,8 @@ class IsAuthenticatedOrHasToken(permissions.IsAuthenticated):
class APIWebhookView(WebhookMixin, APIView): class APIWebhookView(WebhookMixin, APIView):
"""API webhook consumer """
API webhook consumer.
Expects the following JSON:: Expects the following JSON::
@ -263,7 +270,8 @@ class APIWebhookView(WebhookMixin, APIView):
permission_classes = [IsAuthenticatedOrHasToken] permission_classes = [IsAuthenticatedOrHasToken]
def get_project(self, **kwargs): 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 Allow for a user to either be authed to receive a project, or require
the integration token to be specified as a POST argument. the integration token to be specified as a POST argument.
@ -304,7 +312,8 @@ class APIWebhookView(WebhookMixin, APIView):
class WebhookView(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 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 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): 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 = get_object_or_404(
Integration, Integration,
project__slug=project_slug, project__slug=project_slug,

View File

@ -34,7 +34,8 @@ log = logging.getLogger(__name__)
class UserSelectViewSet(viewsets.ModelViewSet): 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`, Viewsets using this class should have an attribute `admin_serializer_class`,
which is a serializer that might have more fields that only admin/staff 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 return self.serializer_class
def get_queryset(self): 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) return self.model.objects.api(self.request.user)
class ProjectViewSet(UserSelectViewSet): class ProjectViewSet(UserSelectViewSet):
"""List, filter, etc. Projects.""" """
List, filter, etc. Projects.
"""
permission_classes = [APIPermission] permission_classes = [APIPermission]
renderer_classes = (JSONRenderer,) renderer_classes = (JSONRenderer,)
@ -131,7 +134,8 @@ class ProjectViewSet(UserSelectViewSet):
@decorators.detail_route(permission_classes=[permissions.IsAdminUser], methods=['post']) @decorators.detail_route(permission_classes=[permissions.IsAdminUser], methods=['post'])
def sync_versions(self, request, **kwargs): 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. 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.permission_classes((permissions.IsAdminUser,))
@decorators.renderer_classes((JSONRenderer,)) @decorators.renderer_classes((JSONRenderer,))
def index_search(request): def index_search(request):
"""Add things to the search index""" """Add things to the search index."""
data = request.data['data'] data = request.data['data']
version_pk = data['version_pk'] version_pk = data['version_pk']
commit = data.get('commit') commit = data.get('commit')
@ -41,7 +41,7 @@ def index_search(request):
@decorators.permission_classes((permissions.AllowAny,)) @decorators.permission_classes((permissions.AllowAny,))
@decorators.renderer_classes((JSONRenderer,)) @decorators.renderer_classes((JSONRenderer,))
def search(request): 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) project_slug = request.GET.get('project', None)
version_slug = request.GET.get('version', LATEST) version_slug = request.GET.get('version', LATEST)
query = request.GET.get('q', None) query = request.GET.get('q', None)
@ -100,7 +100,8 @@ def project_search(request):
@decorators.permission_classes((permissions.AllowAny,)) @decorators.permission_classes((permissions.AllowAny,))
@decorators.renderer_classes((JSONRenderer,)) @decorators.renderer_classes((JSONRenderer,))
def section_search(request): def section_search(request):
"""Section search """
Section search.
Queries with query ``q`` across all documents and projects. Queries can be Queries with query ``q`` across all documents and projects. Queries can be
limited to a single project or version by using the ``project`` and limited to a single project or version by using the ``project`` and
@ -129,7 +130,6 @@ def section_search(request):
Example:: Example::
GET /api/v2/search/section/?q=virtualenv&project=django GET /api/v2/search/section/?q=virtualenv&project=django
""" """
query = request.GET.get('q', None) query = request.GET.get('q', None)
if not query: if not query:

View File

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

View File

@ -15,7 +15,7 @@ from readthedocs.search.signals import (before_project_search,
def search_project(request, query, language=None): def search_project(request, query, language=None):
"""Search index for projects matching query""" """Search index for projects matching query."""
body = { body = {
"query": { "query": {
"bool": { "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): 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 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, def search_section(request, query, project_slug=None, version_slug=LATEST,
path=None): 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 When you search, you will have a ``project`` facet, which includes the
number of matching sections per project. When you search inside a project, 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): 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: if build_dir:
full_path = version.project.full_json_path(version.slug) full_path = version.project.full_json_path(version.slug)
else: else:
@ -215,7 +215,8 @@ def parse_sphinx_sections(content):
def parse_mkdocs_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 May raise a ValueError
""" """