Skip to content

Commit c2fada5

Browse files
committed
More changes to the extended modeling
1 parent b15adf3 commit c2fada5

File tree

5 files changed

+97
-51
lines changed

5 files changed

+97
-51
lines changed

readthedocs/builds/models.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from taggit.managers import TaggableManager
1313

1414
from readthedocs.core.utils import broadcast
15-
from readthedocs.privacy.loader import (VersionManager, RelatedBuildManager,
16-
BuildManager)
15+
from readthedocs.privacy.backend import VersionQuerySet, VersionManager
16+
from readthedocs.privacy.loader import RelatedBuildManager, BuildManager
1717
from readthedocs.projects.models import Project
1818
from readthedocs.projects.constants import (PRIVACY_CHOICES, GITHUB_URL,
1919
GITHUB_REGEXS, BITBUCKET_URL,
@@ -69,7 +69,8 @@ class Version(models.Model):
6969
)
7070
tags = TaggableManager(blank=True)
7171
machine = models.BooleanField(_('Machine Created'), default=False)
72-
objects = VersionManager()
72+
73+
objects = VersionManager.from_queryset(VersionQuerySet)()
7374

7475
class Meta:
7576
unique_together = [('project', 'slug')]

readthedocs/core/utils/extend.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@
66
from django.utils.module_loading import import_by_path
77

88

9+
def get_override_class(proxy_class, default_class):
10+
class_id = '.'.join([
11+
inspect.getmodule(proxy_class).__name__,
12+
proxy_class.__name__
13+
])
14+
class_path = getattr(settings, 'CLASS_OVERRIDES', {}).get(class_id)
15+
if class_path is None and proxy_class._override_setting is not None:
16+
class_path = getattr(settings, proxy_class._override_setting, None)
17+
if class_path is not None:
18+
default_class = import_by_path(class_path)
19+
return default_class
20+
21+
22+
class SettingsOverrideMeta(type):
23+
24+
"""Meta class for passing along classmethod class to the underlying class"""
25+
26+
def __getattr__(cls, attr):
27+
proxy_class = getattr(cls, '_default_class')
28+
return getattr(proxy_class, attr)
29+
30+
931
class SettingsOverrideObject(object):
1032

1133
"""Base class for creating class that can be overridden
@@ -29,25 +51,15 @@ class without monkey patching it. This abstract class allows for lazy
2951
attempt to pull the key :py:cvar:`_override_setting` from ``settings``.
3052
"""
3153

54+
__metaclass__ = SettingsOverrideMeta
55+
3256
_default_class = None
3357
_override_setting = None
3458

3559
def __new__(cls, *args, **kwargs):
3660
"""Set up wrapped object
3761
38-
This is called when attributes are accessed on :py:class:`LazyObject`
39-
and the underlying wrapped object does not yet exist.
62+
Create an instance of the underlying proxy class and return instead of
63+
this class.
4064
"""
41-
cls_new = cls._default_class
42-
cls_path = (getattr(settings, 'CLASS_OVERRIDES', {})
43-
.get(cls._get_class_id()))
44-
if cls_path is None and cls._override_setting is not None:
45-
cls_path = getattr(settings, cls._override_setting, None)
46-
if cls_path is not None:
47-
cls_new = import_by_path(cls_path)
48-
return cls_new(*args, **kwargs)
49-
50-
@classmethod
51-
def _get_class_id(cls):
52-
# type() here, because LazyObject overrides some attribute access
53-
return '.'.join([inspect.getmodule(cls).__name__, cls.__name__])
65+
return get_override_class(cls, cls._default_class)(*args, **kwargs)

readthedocs/privacy/backend.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12

23
from django.db import models
34

@@ -9,8 +10,11 @@
910
from readthedocs.builds.constants import LATEST_VERBOSE_NAME
1011
from readthedocs.builds.constants import STABLE
1112
from readthedocs.builds.constants import STABLE_VERBOSE_NAME
13+
from readthedocs.core.utils.extend import (SettingsOverrideObject,
14+
get_override_class)
1215
from readthedocs.projects import constants
1316

17+
log = logging.getLogger(__name__)
1418

1519
class ProjectManager(models.Manager):
1620

@@ -75,6 +79,47 @@ def api(self, user=None):
7579

7680
class VersionManager(models.Manager):
7781

82+
"""Version manager for manager only queries
83+
84+
For queries not suitable for the :py:cls:`VersionQuerySet`, such as create
85+
queries.
86+
"""
87+
88+
@classmethod
89+
def from_queryset(cls, queryset_class, class_name=None):
90+
queryset_class = get_override_class(
91+
VersionQuerySet,
92+
VersionQuerySet._default_class
93+
)
94+
return super(VersionManager, cls).from_queryset(queryset_class, class_name)
95+
96+
def create_stable(self, **kwargs):
97+
defaults = {
98+
'slug': STABLE,
99+
'verbose_name': STABLE_VERBOSE_NAME,
100+
'machine': True,
101+
'active': True,
102+
'identifier': STABLE,
103+
'type': TAG,
104+
}
105+
defaults.update(kwargs)
106+
return self.create(**defaults)
107+
108+
def create_latest(self, **kwargs):
109+
defaults = {
110+
'slug': LATEST,
111+
'verbose_name': LATEST_VERBOSE_NAME,
112+
'machine': True,
113+
'active': True,
114+
'identifier': LATEST,
115+
'type': BRANCH,
116+
}
117+
defaults.update(kwargs)
118+
return self.create(**defaults)
119+
120+
121+
class VersionQuerySetBase(models.QuerySet):
122+
78123
"""
79124
Versions take into account their own privacy_level setting.
80125
"""
@@ -83,7 +128,7 @@ class VersionManager(models.Manager):
83128

84129
def _add_user_repos(self, queryset, user):
85130
if user.has_perm('builds.view_version'):
86-
return self.get_queryset().all().distinct()
131+
return self.all().distinct()
87132
if user.is_authenticated():
88133
user_queryset = get_objects_for_user(user, 'builds.view_version')
89134
queryset = user_queryset | queryset
@@ -122,30 +167,6 @@ def private(self, user=None, project=None, only_active=True):
122167
def api(self, user=None):
123168
return self.public(user, only_active=False)
124169

125-
def create_stable(self, **kwargs):
126-
defaults = {
127-
'slug': STABLE,
128-
'verbose_name': STABLE_VERBOSE_NAME,
129-
'machine': True,
130-
'active': True,
131-
'identifier': STABLE,
132-
'type': TAG,
133-
}
134-
defaults.update(kwargs)
135-
return self.create(**defaults)
136-
137-
def create_latest(self, **kwargs):
138-
defaults = {
139-
'slug': LATEST,
140-
'verbose_name': LATEST_VERBOSE_NAME,
141-
'machine': True,
142-
'active': True,
143-
'identifier': LATEST,
144-
'type': BRANCH,
145-
}
146-
defaults.update(kwargs)
147-
return self.create(**defaults)
148-
149170
def for_project(self, project):
150171
"""Return all versions for a project, including translations"""
151172
return self.filter(
@@ -154,6 +175,11 @@ def for_project(self, project):
154175
)
155176

156177

178+
class VersionQuerySet(SettingsOverrideObject):
179+
180+
_default_class = VersionQuerySetBase
181+
182+
157183
class BuildManager(models.Manager):
158184

159185
"""

readthedocs/privacy/loader.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
ProjectManager = import_by_path(
66
getattr(settings, 'PROJECT_MANAGER',
77
'readthedocs.privacy.backend.ProjectManager'))
8-
VersionManager = import_by_path(
9-
getattr(settings, 'VERSION_MANAGER',
10-
'readthedocs.privacy.backend.VersionManager'))
8+
# VersionQuerySet was replaced by SettingsOverrideObject
119
BuildManager = import_by_path(
1210
getattr(settings, 'BUILD_MANAGER',
1311
'readthedocs.privacy.backend.BuildManager'))

readthedocs/rtd_tests/tests/test_extend.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.test import TestCase, override_settings
22

3-
from readthedocs.core.utils.extend import SettingsOverrideObject
3+
from readthedocs.core.utils.extend import (SettingsOverrideObject,
4+
get_override_class)
45

56

67
# Top level to ensure module name is correct
@@ -29,10 +30,12 @@ class Foo(SettingsOverrideObject):
2930
_override_setting = 'FOO_OVERRIDE_CLASS'
3031

3132
foo = Foo()
32-
self.assertEqual(foo._get_class_id(), EXTEND_PATH)
3333
self.assertEqual(foo.__class__.__name__, 'FooBase')
3434
self.assertEqual(foo.bar(), 1)
3535

36+
override_class = get_override_class(Foo, Foo._default_class)
37+
self.assertEqual(override_class, FooBase)
38+
3639
@override_settings(FOO_OVERRIDE_CLASS=EXTEND_OVERRIDE_PATH)
3740
def test_with_basic_override(self):
3841
"""Test class override setting defined"""
@@ -41,10 +44,12 @@ class Foo(SettingsOverrideObject):
4144
_override_setting = 'FOO_OVERRIDE_CLASS'
4245

4346
foo = Foo()
44-
self.assertEqual(foo._get_class_id(), EXTEND_PATH)
4547
self.assertEqual(foo.__class__.__name__, 'NewFoo')
4648
self.assertEqual(foo.bar(), 2)
4749

50+
override_class = get_override_class(Foo, Foo._default_class)
51+
self.assertEqual(override_class, NewFoo)
52+
4853
@override_settings(FOO_OVERRIDE_CLASS=None,
4954
CLASS_OVERRIDES={
5055
EXTEND_PATH: EXTEND_OVERRIDE_PATH,
@@ -56,10 +61,12 @@ class Foo(SettingsOverrideObject):
5661
_override_setting = 'FOO_OVERRIDE_CLASS'
5762

5863
foo = Foo()
59-
self.assertEqual(foo._get_class_id(), EXTEND_PATH)
6064
self.assertEqual(foo.__class__.__name__, 'NewFoo')
6165
self.assertEqual(foo.bar(), 2)
6266

67+
override_class = get_override_class(Foo, Foo._default_class)
68+
self.assertEqual(override_class, NewFoo)
69+
6370
@override_settings(FOO_OVERRIDE_CLASS=None,
6471
CLASS_OVERRIDES={
6572
EXTEND_PATH: EXTEND_OVERRIDE_PATH,
@@ -70,6 +77,8 @@ class Foo(SettingsOverrideObject):
7077
_default_class = FooBase
7178

7279
foo = Foo()
73-
self.assertEqual(foo._get_class_id(), EXTEND_PATH)
7480
self.assertEqual(foo.__class__.__name__, 'NewFoo')
7581
self.assertEqual(foo.bar(), 2)
82+
83+
override_class = get_override_class(Foo, Foo._default_class)
84+
self.assertEqual(override_class, NewFoo)

0 commit comments

Comments
 (0)