Skip to content

Commit fa72754

Browse files
committed
Merge remote-tracking branch 'origin/master' into intersphinx-modeling
2 parents ab7a30c + 9006697 commit fa72754

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+14328
-14373
lines changed

docs/_static/css/sphinx_prompt_css.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ pre.highlight {
77
padding: 12px 12px;
88
}
99

10-
pre.highlight span.prompt1 {
10+
/* Tweak css for sphinx_prompt #5281 */
11+
pre.highlight span[class^="prompt"] {
1112
font-size: 12px;
1213
line-height: 1.4;
1314
}

docs/features/sitemaps.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Sitemaps
2+
========
3+
4+
Sitemaps_ allows us to inform search engines about URLs that are available for crawling
5+
and communicate them additional information about each URL of the project:
6+
7+
* when it was last updated,
8+
* how often it changes,
9+
* how important it is in relation to other URLs in the site, and
10+
* what translations are available for a page.
11+
12+
Read the Docs automatically generates a sitemap for each project that hosts
13+
to improve results when performing a search on these search engines.
14+
This allow us to prioritize results based on the version number, for example
15+
to show ``latest`` as the top result followed by ``stable`` and then all the project's
16+
versions sorted following semver_.
17+
18+
.. _semver: https://semver.org/
19+
.. _Sitemaps: https://www.sitemaps.org/

docs/gsoc.rst

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
Google Summer of Code
22
=====================
33

4-
..
5-
.. note:: Thanks for your interest in Read the Docs!
6-
We are working hard to update the ideas list now that we are accepted in GSOC.
7-
Please give us a little while to work on things,
8-
and check back on this page for updates.
4+
.. note:: Thanks for your interest in Read the Docs!
5+
Please follow the instructions in `Getting Started`_,
6+
as a good place to start.
7+
**Contacting us will not increase your chance of being accepted,
8+
but opening Pull Requests with docs and tests will.**
99

10-
Read the Docs is hoping to be in the Google Summer of Code in 2019.
10+
Read the Docs is excited to be in the Google Summer of Code in 2019.
1111
This page will contain all the information for students and anyone else interested in helping.
1212

1313
Skills
@@ -26,18 +26,6 @@ We're happy to help you get up to speed,
2626
but the more you are able to demonstrate ability in advance,
2727
the more likely we are to choose your application!
2828

29-
Mentors
30-
-------
31-
32-
Currently we have a few folks signed up:
33-
34-
* Eric Holscher
35-
* Manuel Kaufmann
36-
* Anthony Johnson
37-
38-
.. warning:: Please do not reach out directly to anyone about the Summer of Code.
39-
It will **not** increase your chances of being accepted!
40-
4129
Getting Started
4230
---------------
4331

@@ -53,6 +41,19 @@ Want to get involved?
5341

5442
If you're interested in participating in GSoC as a student, you can apply during the normal process provided by Google. We are currently overwhelmed with interest, so we are not able to respond individually to each person who is interested.
5543

44+
Mentors
45+
-------
46+
47+
Currently we have a few folks signed up:
48+
49+
* Eric Holscher
50+
* Manuel Kaufmann
51+
* Anthony Johnson
52+
* Safwan Rahman
53+
54+
.. warning:: Please do not reach out directly to anyone about the Summer of Code.
55+
It will **not** increase your chances of being accepted!
56+
5657
Project Ideas
5758
-------------
5859

docs/settings.rst

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,6 @@ Default: :djangosetting:`DOCUMENT_PYQUERY_PATH`
6767
The Pyquery path to an HTML element that is the root of your document.
6868
This is used for making sure we are only searching the main content of a document.
6969

70-
USE_PIP_INSTALL
71-
---------------
72-
73-
Default: :djangosetting:`USE_PIP_INSTALL`
74-
75-
Whether to use `pip install .` or `python setup.py install` when installing packages into the Virtualenv. Default is to use `python setup.py install`.
76-
77-
7870
PUBLIC_DOMAIN
7971
-------------
8072

@@ -177,4 +169,4 @@ This setting is used for automatically indexing objects to elasticsearch.
177169
project and build documentations without having elasticsearch.
178170

179171

180-
.. _elasticsearch-dsl-py.connections.configure: https://elasticsearch-dsl.readthedocs.io/en/stable/configuration.html#multiple-clusters
172+
.. _elasticsearch-dsl-py.connections.configure: https://elasticsearch-dsl.readthedocs.io/en/stable/configuration.html#multiple-clusters

docs/tests.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,19 @@ argument::
2222

2323
tox "'--including-search'"
2424

25+
.. warning::
26+
27+
Running tests for search needs an Elasticsearch :ref:`instance running locally <development/search:Installing and running Elasticsearch>`.
28+
2529
To target a specific environment::
2630

27-
tox -e py27
31+
tox -e py36
2832

2933
The ``tox`` configuration has the following environments configured. You can
3034
target a single environment to limit the test suite::
3135

32-
py27
33-
Run our test suite using Python 2.7
36+
py36
37+
Run our test suite using Python 3.6
3438

3539
lint
3640
Run code linting using `Prospector`_. This currently runs `pylint`_,

media/css/core.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,14 @@ p.build-missing { font-size: .8em; color: #9d9a55; margin: 0 0 3px; }
702702
#footer label { color: #BCC1C3; font-weight: normal; }
703703
#footer input[type="text"], #footer input[type="email"] { padding: 4px; font-size: 12px; line-height: 16px; margin-bottom: 5px }
704704

705+
/* Warning Icon for Build List triggered */
706+
.module-item.col-span a span.icon-warning:before {
707+
font-family: FontAwesome;
708+
font-size: .9em;
709+
padding-right: .3em;
710+
font-weight: normal;
711+
content: "\f071";
712+
}
705713

706714
/* utils */
707715

readthedocs/builds/admin.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from readthedocs.builds.models import Build, BuildCommandResult, Version
99
from readthedocs.core.utils import trigger_build
10+
from readthedocs.core.utils.general import wipe_version_via_slugs
1011

1112

1213
class BuildCommandResultInline(admin.TabularInline):
@@ -57,7 +58,22 @@ class VersionAdmin(GuardedModelAdmin):
5758
list_filter = ('type', 'privacy_level', 'active', 'built')
5859
search_fields = ('slug', 'project__slug')
5960
raw_id_fields = ('project',)
60-
actions = ['build_version']
61+
actions = ['wipe_selected_versions', 'build_version']
62+
63+
def wipe_selected_versions(self, request, queryset):
64+
"""Wipes the selected versions."""
65+
for version in queryset:
66+
wipe_version_via_slugs(
67+
version_slug=version.slug,
68+
project_slug=version.project.slug
69+
)
70+
self.message_user(
71+
request,
72+
'Wiped {}.'.format(version.slug),
73+
level=messages.SUCCESS
74+
)
75+
76+
wipe_selected_versions.short_description = 'Wipe selected versions'
6177

6278
def build_version(self, request, queryset):
6379
"""Trigger a build for the project version."""

readthedocs/builds/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
"""Models for the builds app."""
44

5+
import datetime
56
import logging
67
import os.path
78
import re
@@ -31,6 +32,7 @@
3132
BRANCH,
3233
BUILD_STATE,
3334
BUILD_STATE_FINISHED,
35+
BUILD_STATE_TRIGGERED,
3436
BUILD_TYPES,
3537
LATEST,
3638
NON_REPOSITORY_VERSIONS,
@@ -629,6 +631,12 @@ def finished(self):
629631
"""Return if build has a finished state."""
630632
return self.state == BUILD_STATE_FINISHED
631633

634+
@property
635+
def is_stale(self):
636+
"""Return if build state is triggered & date more than 5m ago."""
637+
mins_ago = timezone.now() - datetime.timedelta(minutes=5)
638+
return self.state == BUILD_STATE_TRIGGERED and self.date < mins_ago
639+
632640

633641
class BuildCommandResultMixin:
634642

readthedocs/builds/syncers.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
31
"""
42
Classes to copy files between build and web servers.
53
@@ -13,12 +11,15 @@
1311
import shutil
1412

1513
from django.conf import settings
14+
from django.core.exceptions import SuspiciousFileOperation
15+
from django.core.files.storage import get_storage_class
1616

1717
from readthedocs.core.utils import safe_makedirs
1818
from readthedocs.core.utils.extend import SettingsOverrideObject
1919

2020

2121
log = logging.getLogger(__name__)
22+
storage = get_storage_class()()
2223

2324

2425
class BaseSyncer:
@@ -42,6 +43,11 @@ def copy(cls, path, target, is_file=False, **kwargs):
4243
return
4344
if os.path.exists(target):
4445
os.remove(target)
46+
47+
# Create containing directory if it doesn't exist
48+
directory = os.path.dirname(target)
49+
safe_makedirs(directory)
50+
4551
shutil.copy2(path, target)
4652
else:
4753
if os.path.exists(target):
@@ -143,6 +149,10 @@ def copy(cls, path, target, host, is_file=False, **kwargs): # pylint: disable=a
143149
log.info('Remote Pull %s to %s', path, target)
144150
if not is_file and not os.path.exists(target):
145151
safe_makedirs(target)
152+
if is_file:
153+
# Create containing directory if it doesn't exist
154+
directory = os.path.dirname(target)
155+
safe_makedirs(directory)
146156
# Add a slash when copying directories
147157
sync_cmd = "rsync -e 'ssh -T' -av --delete {user}@{host}:{path} {target}".format(
148158
host=host,
@@ -159,6 +169,59 @@ def copy(cls, path, target, host, is_file=False, **kwargs): # pylint: disable=a
159169
)
160170

161171

172+
class SelectiveStorageRemotePuller(RemotePuller):
173+
174+
"""
175+
Like RemotePuller but certain files are copied via Django's storage system.
176+
177+
If a file with extensions specified by ``extensions`` is copied, it will be copied to storage
178+
and the original is removed.
179+
180+
See: https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-DEFAULT_FILE_STORAGE
181+
"""
182+
183+
extensions = ('.pdf', '.epub', '.zip')
184+
185+
@classmethod
186+
def get_storage_path(cls, path):
187+
"""
188+
Gets the path to the file within the storage engine.
189+
190+
For example, if the path was $MEDIA_ROOT/pdfs/latest.pdf
191+
the storage_path is 'pdfs/latest.pdf'
192+
193+
:raises: SuspiciousFileOperation if the path isn't under settings.MEDIA_ROOT
194+
"""
195+
path = os.path.normpath(path)
196+
if not path.startswith(settings.MEDIA_ROOT):
197+
raise SuspiciousFileOperation
198+
199+
path = path.replace(settings.MEDIA_ROOT, '').lstrip('/')
200+
return path
201+
202+
@classmethod
203+
def copy(cls, path, target, host, is_file=False, **kwargs): # pylint: disable=arguments-differ
204+
RemotePuller.copy(path, target, host, is_file, **kwargs)
205+
206+
if getattr(storage, 'write_build_media', False):
207+
# This is a sanity check for the case where
208+
# storage is backed by the local filesystem
209+
# In that case, removing the original target file locally
210+
# would remove the file from storage as well
211+
212+
if is_file and os.path.exists(target) and \
213+
any([target.lower().endswith(ext) for ext in cls.extensions]):
214+
log.info('Selective Copy %s to media storage', target)
215+
216+
storage_path = cls.get_storage_path(target)
217+
218+
if storage.exists(storage_path):
219+
storage.delete(storage_path)
220+
221+
with open(target, 'rb') as fd:
222+
storage.save(storage_path, fd)
223+
224+
162225
class Syncer(SettingsOverrideObject):
163226
_default_class = LocalSyncer
164227
_override_setting = 'FILE_SYNCER'

readthedocs/builds/version_slug.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from django.db import models
2828
from django.utils.encoding import force_text
29+
from slugify import slugify as unicode_slugify
2930

3031

3132
def get_fields_with_model(cls):
@@ -53,13 +54,15 @@ def get_fields_with_model(cls):
5354

5455
class VersionSlugField(models.CharField):
5556

56-
"""Inspired by ``django_extensions.db.fields.AutoSlugField``."""
57+
"""
58+
Inspired by ``django_extensions.db.fields.AutoSlugField``.
5759
58-
invalid_chars_re = re.compile('[^-._a-z0-9]')
59-
leading_punctuation_re = re.compile('^[-._]+')
60-
placeholder = '-'
61-
fallback_slug = 'unknown'
60+
Uses ``unicode-slugify`` to generate the slug.
61+
"""
62+
63+
ok_chars = '-._' # dash, dot, underscore
6264
test_pattern = re.compile('^{pattern}$'.format(pattern=VERSION_SLUG_REGEX))
65+
fallback_slug = 'unknown'
6366

6467
def __init__(self, *args, **kwargs):
6568
kwargs.setdefault('db_index', True)
@@ -78,13 +81,42 @@ def get_queryset(self, model_cls, slug_field):
7881
return model._default_manager.all()
7982
return model_cls._default_manager.all()
8083

84+
def _normalize(self, content):
85+
"""
86+
Normalize some invalid characters (/, %, !, ?) to become a dash (``-``).
87+
88+
.. note::
89+
90+
We replace these characters to a dash to keep compatibility with the
91+
old behavior and also because it makes this more readable.
92+
93+
For example, ``release/1.0`` will become ``release-1.0``.
94+
"""
95+
return re.sub('[/%!?]', '-', content)
96+
8197
def slugify(self, content):
98+
"""
99+
Make ``content`` a valid slug.
100+
101+
It uses ``unicode-slugify`` behind the scenes which works properly with
102+
Unicode characters.
103+
"""
82104
if not content:
83105
return ''
84106

85-
slugified = content.lower()
86-
slugified = self.invalid_chars_re.sub(self.placeholder, slugified)
87-
slugified = self.leading_punctuation_re.sub('', slugified)
107+
normalized = self._normalize(content)
108+
slugified = unicode_slugify(
109+
normalized,
110+
only_ascii=True,
111+
spaces=False,
112+
lower=True,
113+
ok=self.ok_chars,
114+
space_replacement='-',
115+
)
116+
117+
# Remove first character wile it's an invalid character for the
118+
# beginning of the slug
119+
slugified = slugified.lstrip(self.ok_chars)
88120

89121
if not slugified:
90122
return self.fallback_slug

0 commit comments

Comments
 (0)