Repository: readthedocs/readthedocs.org

Issue:
Replace extension pattern with updated version
We have two extension patterns utilized currently. These extension points are required so that we can add additional functionality or altered querysets on readthedocs.com.

The modern version is in `readthedocs.core.utils.extend`, and an example of this extension is `readthedocs.core.symlink.PublicSymlink`. There are a number of classes using arbitrary settings to allow for override in `readthedocs.privacy.backend`. Unifying these two implementations would clean things up and consolidate our settings.

Ref #2611 
Replace extension pattern with updated version
We have two extension patterns utilized currently. These extension points are required so that we can add additional functionality or altered querysets on readthedocs.com.

The modern version is in `readthedocs.core.utils.extend`, and an example of this extension is `readthedocs.core.symlink.PublicSymlink`. There are a number of classes using arbitrary settings to allow for override in `readthedocs.privacy.backend`. Unifying these two implementations would clean things up and consolidate our settings.

Ref #2611 


Discussion:
`readthedocs.privacy.backend` already uses `SettingsOverrideObject` with `VersionManager`/`VersionQuerySet` - what else do you see as requiring changes?
The old pattern is used in these spots:

* https://github.com/rtfd/readthedocs.org/blob/master/readthedocs/privacy/loader.py
* https://github.com/rtfd/readthedocs.org/blob/c84cd7362a5c43eb95c2316fef9b6df4c592b3b4/readthedocs/oauth/services/__init__.py
* https://github.com/rtfd/readthedocs.org/blob/c84cd7362a5c43eb95c2316fef9b6df4c592b3b4/readthedocs/projects/backends/views.py
`readthedocs.privacy.backend` already uses `SettingsOverrideObject` with `VersionManager`/`VersionQuerySet` - what else do you see as requiring changes?
The old pattern is used in these spots:

* https://github.com/rtfd/readthedocs.org/blob/master/readthedocs/privacy/loader.py
* https://github.com/rtfd/readthedocs.org/blob/c84cd7362a5c43eb95c2316fef9b6df4c592b3b4/readthedocs/oauth/services/__init__.py
* https://github.com/rtfd/readthedocs.org/blob/c84cd7362a5c43eb95c2316fef9b6df4c592b3b4/readthedocs/projects/backends/views.py

Resolving patch:
diff --git a/readthedocs/oauth/services/__init__.py b/readthedocs/oauth/services/__init__.py
--- a/readthedocs/oauth/services/__init__.py
+++ b/readthedocs/oauth/services/__init__.py
@@ -1,13 +1,17 @@
 """Conditional classes for OAuth services"""
 
-from django.utils.module_loading import import_string
-from django.conf import settings
-
-GitHubService = import_string(
-    getattr(settings, 'OAUTH_GITHUB_SERVICE',
-            'readthedocs.oauth.services.github.GitHubService'))
-BitbucketService = import_string(
-    getattr(settings, 'OAUTH_BITBUCKET_SERVICE',
-            'readthedocs.oauth.services.bitbucket.BitbucketService'))
+from readthedocs.core.utils.extend import SettingsOverrideObject
+from readthedocs.oauth.services import github, bitbucket
+
+
+class GitHubService(SettingsOverrideObject):
+    _default_class = github.GitHubService
+    _override_setting = 'OAUTH_GITHUB_SERVICE'
+
+
+class BitbucketService(SettingsOverrideObject):
+    _default_class = bitbucket.BitbucketService
+    _override_setting = 'OAUTH_BITBUCKET_SERVICE'
+
 
 registry = [GitHubService, BitbucketService]
diff --git a/readthedocs/privacy/loader.py b/readthedocs/privacy/loader.py
--- a/readthedocs/privacy/loader.py
+++ b/readthedocs/privacy/loader.py
@@ -1,36 +1,54 @@
-from django.utils.module_loading import import_string
-from django.conf import settings
+from readthedocs.core.utils.extend import SettingsOverrideObject
+from readthedocs.privacy import backend
+
 
 # Managers
-ProjectManager = import_string(
-    getattr(settings, 'PROJECT_MANAGER',
-            'readthedocs.privacy.backend.ProjectManager'))
+from readthedocs.privacy.backends import syncers
+
+
+class ProjectManager(SettingsOverrideObject):
+    _default_class = backend.ProjectManager
+    _override_setting = 'PROJECT_MANAGER'
+
+
 # VersionQuerySet was replaced by SettingsOverrideObject
-VersionManager = import_string(
-    getattr(settings, 'VERSION_MANAGER',
-            'readthedocs.privacy.backend.VersionManager'))
-BuildManager = import_string(
-    getattr(settings, 'BUILD_MANAGER',
-            'readthedocs.privacy.backend.BuildManager'))
-RelatedProjectManager = import_string(
-    getattr(settings, 'RELATED_PROJECT_MANAGER',
-            'readthedocs.privacy.backend.RelatedProjectManager'))
-RelatedBuildManager = import_string(
-    getattr(settings, 'RELATED_BUILD_MANAGER',
-            'readthedocs.privacy.backend.RelatedBuildManager'))
-RelatedUserManager = import_string(
-    getattr(settings, 'RELATED_USER_MANAGER',
-            'readthedocs.privacy.backend.RelatedUserManager'))
-ChildRelatedProjectManager = import_string(
-    getattr(settings, 'CHILD_RELATED_PROJECT_MANAGER',
-            'readthedocs.privacy.backend.ChildRelatedProjectManager'))
+class VersionManager(SettingsOverrideObject):
+    _default_class = backend.VersionManager
+    _override_setting = 'VERSION_MANAGER'
+
+
+class BuildManager(SettingsOverrideObject):
+    _default_class = backend.BuildManager
+    _override_setting = 'BUILD_MANAGER'
+
+
+class RelatedProjectManager(SettingsOverrideObject):
+    _default_class = backend.RelatedProjectManager
+    _override_setting = 'RELATED_PROJECT_MANAGER'
+
+
+class RelatedBuildManager(SettingsOverrideObject):
+    _default_class = backend.RelatedBuildManager
+    _override_setting = 'RELATED_BUILD_MANAGER'
+
+
+class RelatedUserManager(SettingsOverrideObject):
+    _default_class = backend.RelatedUserManager
+    _override_setting = 'RELATED_USER_MANAGER'
+
+
+class ChildRelatedProjectManager(SettingsOverrideObject):
+    _default_class = backend.ChildRelatedProjectManager
+    _override_setting = 'CHILD_RELATED_PROJECT_MANAGER'
+
 
 # Permissions
-AdminPermission = import_string(
-    getattr(settings, 'ADMIN_PERMISSION',
-            'readthedocs.privacy.backend.AdminPermission'))
+class AdminPermission(SettingsOverrideObject):
+    _default_class = backend.AdminPermission
+    _override_setting = 'ADMIN_PERMISSION'
+
 
 # Syncers
-Syncer = import_string(
-    getattr(settings, 'FILE_SYNCER',
-            'readthedocs.privacy.backends.syncers.LocalSyncer'))
+class Syncer(SettingsOverrideObject):
+    _default_class = syncers.LocalSyncer
+    _override_setting = 'FILE_SYNCER'
diff --git a/readthedocs/projects/backends/views.py b/readthedocs/projects/backends/views.py
--- a/readthedocs/projects/backends/views.py
+++ b/readthedocs/projects/backends/views.py
@@ -5,20 +5,17 @@
 settings override of the view class.
 """
 
-from django.utils.module_loading import import_string
-from django.conf import settings
+from readthedocs.core.utils.extend import SettingsOverrideObject
+from readthedocs.projects.views import private
 
 
 # Project Import Wizard
-ImportWizardView = import_string(getattr(
-    settings,
-    'PROJECT_IMPORT_VIEW',
-    'readthedocs.projects.views.private.ImportWizardView'
-))
+class ImportWizardView(SettingsOverrideObject):
+    _default_class = private.ImportWizardView
+    _override_setting = 'PROJECT_IMPORT_VIEW'
+
 
 # Project demo import
-ImportDemoView = import_string(getattr(
-    settings,
-    'PROJECT_IMPORT_DEMO_VIEW',
-    'readthedocs.projects.views.private.ImportDemoView'
-))
+class ImportDemoView(SettingsOverrideObject):
+    _default_class = private.ImportDemoView
+    _override_setting = 'PROJECT_IMPORT_DEMO_VIEW'


Test patch: