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\ ; donemore-gsoc
parent
63a96d7d61
commit
4d10f2e287
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue