From ef05c6a353825b2b248677a2f85c9724544be04d Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:17:21 -0700
Subject: [PATCH 01/40] Add Domain model
---
readthedocs/projects/admin.py | 8 ++++-
.../migrations/0004_add_cname_modeling.py | 33 +++++++++++++++++++
readthedocs/projects/models.py | 15 +++++++++
3 files changed, 55 insertions(+), 1 deletion(-)
create mode 100644 readthedocs/projects/migrations/0004_add_cname_modeling.py
diff --git a/readthedocs/projects/admin.py b/readthedocs/projects/admin.py
index 47e6469d0fb..f9a0cd44cd7 100644
--- a/readthedocs/projects/admin.py
+++ b/readthedocs/projects/admin.py
@@ -5,7 +5,8 @@
from readthedocs.builds.models import Version
from django.contrib import admin
from readthedocs.redirects.models import Redirect
-from readthedocs.projects.models import (Project, ImportedFile, ProjectRelationship, EmailHook, WebHook)
+from .models import (Project, ImportedFile,
+ ProjectRelationship, EmailHook, WebHook, Domain)
from guardian.admin import GuardedModelAdmin
@@ -37,7 +38,12 @@ class ImportedFileAdmin(admin.ModelAdmin):
list_display = ('path', 'name', 'version')
+class DomainAdmin(admin.ModelAdmin):
+ list_display = ('url', 'project')
+ model = Domain
+
admin.site.register(Project, ProjectAdmin)
admin.site.register(ImportedFile, ImportedFileAdmin)
+admin.site.register(Domain, DomainAdmin)
admin.site.register(EmailHook)
admin.site.register(WebHook)
diff --git a/readthedocs/projects/migrations/0004_add_cname_modeling.py b/readthedocs/projects/migrations/0004_add_cname_modeling.py
new file mode 100644
index 00000000000..d5b0f0f9924
--- /dev/null
+++ b/readthedocs/projects/migrations/0004_add_cname_modeling.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0003_project_cdn_enabled'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Domain',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('url', models.URLField(unique=True, verbose_name='Name')),
+ ('canonical', models.BooleanField(default=False, help_text='This URL is where the documentation is served from.', verbose_name='Canonical')),
+ ('active', models.BooleanField(default=False, help_text='This is an active domain for this project.', verbose_name='Default')),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='documentation_type',
+ field=models.CharField(default=b'sphinx', help_text='Type of documentation you are building. More info .', max_length=20, verbose_name='Documentation type', choices=[(b'auto', 'Automatically Choose'), (b'sphinx', 'Sphinx Html'), (b'mkdocs', 'Mkdocs (Markdown)'), (b'sphinx_htmldir', 'Sphinx HtmlDir'), (b'sphinx_singlehtml', 'Sphinx Single Page HTML')]),
+ ),
+ migrations.AddField(
+ model_name='domain',
+ name='project',
+ field=models.ForeignKey(related_name='domains', to='projects.Project'),
+ ),
+ ]
diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py
index bedb99004f5..593580de89b 100644
--- a/readthedocs/projects/models.py
+++ b/readthedocs/projects/models.py
@@ -898,3 +898,18 @@ class WebHook(Notification):
def __unicode__(self):
return self.url
+
+
+class Domain(models.Model):
+ project = models.ForeignKey(Project, related_name='domains')
+ objects = RelatedProjectManager()
+
+ url = models.URLField(_('URL'), unique=True)
+ canonical = models.BooleanField(_('Canonical'), default=False,
+ help_text=_('This URL is where the documentation is served from.'))
+ active = models.BooleanField(
+ _('Active'), default=False,
+ help_text=_('This is an active domain for this project.'))
+
+ def __unicode__(self):
+ return "{url} pointed at {project}".format(url=self.url, project=self.project.name)
From eba84ea29b2569e3a4226298ab64ff4500d1088f Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:20:06 -0700
Subject: [PATCH 02/40] Add Admin views for Domains
---
readthedocs/core/mixins.py | 13 +++++
readthedocs/projects/forms.py | 25 +++++++++-
readthedocs/projects/urls/private.py | 22 ++++++++-
readthedocs/projects/views/base.py | 48 ++++++++++++++++++-
readthedocs/projects/views/private.py | 33 +++++++++++--
readthedocs/restapi/serializers.py | 16 ++++++-
.../templates/projects/project_edit_base.html | 1 +
requirements/pip.txt | 1 +
8 files changed, 151 insertions(+), 8 deletions(-)
diff --git a/readthedocs/core/mixins.py b/readthedocs/core/mixins.py
index dd10da57967..e19398877c1 100644
--- a/readthedocs/core/mixins.py
+++ b/readthedocs/core/mixins.py
@@ -4,8 +4,11 @@
from django.conf import settings
+from vanilla import ListView
+
class StripeMixin(object):
+
'''Adds Stripe publishable key to the context data'''
def get_context_data(self, **kwargs):
@@ -14,3 +17,13 @@ def get_context_data(self, **kwargs):
'publishable': settings.STRIPE_PUBLISHABLE,
})
return context
+
+
+class ListViewWithForm(ListView):
+
+ '''List view that also exposes a create form'''
+
+ def get_context_data(self, **kwargs):
+ context = super(ListViewWithForm, self).get_context_data(**kwargs)
+ context['form'] = self.get_form(data=None, files=None)
+ return context
diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py
index 4b5140ce846..22b04d5aeff 100644
--- a/readthedocs/projects/forms.py
+++ b/readthedocs/projects/forms.py
@@ -13,7 +13,7 @@
from readthedocs.core.utils import trigger_build
from readthedocs.redirects.models import Redirect
from readthedocs.projects import constants
-from readthedocs.projects.models import Project, EmailHook, WebHook
+from readthedocs.projects.models import Project, EmailHook, WebHook, Domain
from readthedocs.privacy.loader import AdminPermission
@@ -33,6 +33,7 @@ def save(self, commit=True):
class ProjectTriggerBuildMixin(object):
+
'''Mixin to trigger build on form save
This should be replaced with signals instead of calling trigger_build
@@ -48,11 +49,13 @@ def save(self, commit=True):
class ProjectBackendForm(forms.Form):
+
'''Get the import backend'''
backend = forms.CharField()
class ProjectBasicsForm(ProjectForm):
+
'''Form for basic project fields'''
class Meta:
@@ -237,7 +240,7 @@ def build_versions_form(project):
attrs = {
'project': project,
}
- versions_qs = project.versions.all() # Admin page, so show all versions
+ versions_qs = project.versions.all() # Admin page, so show all versions
active = versions_qs.filter(active=True)
if active.exists():
choices = [(version.slug, version.verbose_name) for version in active]
@@ -389,6 +392,7 @@ def save(self):
self.project.webhook_notifications.add(self.webhook)
return self.project
+
class TranslationForm(forms.Form):
project = forms.CharField()
@@ -429,3 +433,20 @@ def save(self, *args, **kwargs):
)
return redirect
+
+class DomainForm(forms.ModelForm):
+
+ class Meta:
+ model = Domain
+ exclude = ['project']
+
+ def __init__(self, *args, **kwargs):
+ self.project = kwargs.pop('project', None)
+ super(DomainForm, self).__init__(*args, **kwargs)
+
+ def save(self, *args, **kwargs):
+ domain = Domain.objects.create(
+ project=self.project,
+ **self.cleaned_data
+ )
+ return domain
diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py
index 9e991b949e6..434624bd7fa 100644
--- a/readthedocs/projects/urls/private.py
+++ b/readthedocs/projects/urls/private.py
@@ -1,6 +1,8 @@
from django.conf.urls import patterns, url
-from readthedocs.projects.views.private import AliasList, ProjectDashboard, ImportView
+from readthedocs.projects.views.private import (
+ AliasList, ProjectDashboard, ImportView,
+ DomainList, DomainCreate, DomainDelete, DomainUpdate)
from readthedocs.projects.backends.views import ImportWizardView, ImportDemoView
@@ -120,3 +122,21 @@
'readthedocs.projects.views.private.project_redirects_delete',
name='projects_redirects_delete'),
)
+
+domain_urls = patterns(
+ '',
+ url(r'^(?P[-\w]+)/domains/$',
+ DomainList.as_view(),
+ name='projects_domains'),
+ url(r'^(?P[-\w]+)/domains/create/$',
+ DomainCreate.as_view(),
+ name='projects_domains_create'),
+ url(r'^(?P[-\w]+)/domains/(?P[-\w]+)/edit/$',
+ DomainUpdate.as_view(),
+ name='projects_domains_edit'),
+ url(r'^(?P[-\w]+)/domains/(?P[-\w]+)/delete/$',
+ DomainDelete.as_view(),
+ name='projects_domains_delete'),
+)
+
+urlpatterns += domain_urls
diff --git a/readthedocs/projects/views/base.py b/readthedocs/projects/views/base.py
index f651a7afa1e..1d385530227 100644
--- a/readthedocs/projects/views/base.py
+++ b/readthedocs/projects/views/base.py
@@ -1,7 +1,11 @@
-from readthedocs.projects.models import Project
+from django.shortcuts import get_object_or_404
+from django.core.urlresolvers import reverse
+
+from readthedocs.projects.models import Project, Domain
class ProjectOnboardMixin(object):
+
'''Add project onboard context data to project object views'''
def get_context_data(self, **kwargs):
@@ -25,3 +29,45 @@ def get_context_data(self, **kwargs):
context['onboard'] = onboard
return context
+
+
+# Mixins
+class ProjectAdminMixin(object):
+
+ '''Mixin class that provides project sublevel objects
+
+ This mixin uses several class level variables
+
+ project_url_field
+ The URL kwarg name for the organization slug
+
+ '''
+
+ project_url_field = 'project'
+
+ def get_queryset(self):
+ self.project = self.get_project()
+ return self.model.objects.filter(project=self.project)
+
+ def get_project(self):
+ '''Return organization determined by url kwarg'''
+ if self.project_url_field not in self.kwargs:
+ return None
+ return get_object_or_404(
+ Project.objects.for_admin_user(user=self.request.user),
+ slug=self.kwargs[self.project_url_field]
+ )
+
+ def get_context_data(self, **kwargs):
+ '''Add organization to context data'''
+ context = super(ProjectAdminMixin, self).get_context_data(**kwargs)
+ context['project'] = self.get_project()
+ return context
+
+ def get_form(self, data=None, files=None, **kwargs):
+ '''Pass in organization to form class instance'''
+ kwargs['project'] = self.get_project()
+ return self.form_class(data, files, **kwargs)
+
+ def get_success_url(self, **kwargs):
+ return reverse('projects_domains', args=[self.get_project().slug])
diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py
index 8309887ec38..362f9ae3dce 100644
--- a/readthedocs/projects/views/private.py
+++ b/readthedocs/projects/views/private.py
@@ -10,13 +10,15 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404, render_to_response, render
from django.template import RequestContext
-from django.views.generic import View, ListView, TemplateView
+from django.views.generic import View, TemplateView, ListView
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from formtools.wizard.views import SessionWizardView
from allauth.socialaccount.models import SocialToken
from requests_oauthlib import OAuth2Session
+from vanilla import CreateView, DeleteView, UpdateView
+
from readthedocs.bookmarks.models import Bookmark
from readthedocs.builds import utils as build_utils
@@ -25,14 +27,16 @@
from readthedocs.builds.filters import VersionFilter
from readthedocs.builds.models import VersionAlias
from readthedocs.core.utils import trigger_build
+from readthedocs.core.mixins import ListViewWithForm
from readthedocs.oauth.models import GithubProject, BitbucketProject
from readthedocs.oauth import utils as oauth_utils
from readthedocs.projects.forms import (
ProjectBackendForm, ProjectBasicsForm, ProjectExtraForm,
ProjectAdvancedForm, UpdateProjectForm, SubprojectForm,
build_versions_form, UserForm, EmailHookForm, TranslationForm,
- RedirectForm, WebHookForm)
-from readthedocs.projects.models import Project, EmailHook, WebHook
+ RedirectForm, WebHookForm, DomainForm)
+from readthedocs.projects.models import Project, EmailHook, WebHook, Domain
+from readthedocs.projects.views.base import ProjectAdminMixin
from readthedocs.projects import constants, tasks
@@ -42,6 +46,7 @@
class LoginRequiredMixin(object):
+
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(LoginRequiredMixin, self).dispatch(*args, **kwargs)
@@ -313,6 +318,7 @@ def post(self, request, *args, **kwargs):
class ImportDemoView(PrivateViewMixin, View):
+
'''View to pass request on to import form to import demo project'''
form_class = ProjectBasicsForm
@@ -672,3 +678,24 @@ def project_version_delete_html(request, project_slug, version_slug):
raise Http404
return HttpResponseRedirect(
reverse('project_version_list', kwargs={'project_slug': project_slug}))
+
+
+class DomainList(ProjectAdminMixin, ListViewWithForm):
+ template_name = 'projects/domain_list.html'
+ model = Domain
+ form_class = DomainForm
+
+
+class DomainCreate(ProjectAdminMixin, CreateView):
+ model = Domain
+ form_class = DomainForm
+
+
+class DomainUpdate(ProjectAdminMixin, UpdateView):
+ model = Domain
+ form_class = DomainForm
+
+
+class DomainDelete(ProjectAdminMixin, DeleteView):
+ model = Domain
+
diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py
index a97dc0918aa..8ed9eed3448 100644
--- a/readthedocs/restapi/serializers.py
+++ b/readthedocs/restapi/serializers.py
@@ -1,7 +1,7 @@
from rest_framework import serializers
from readthedocs.builds.models import Build, Version
-from readthedocs.projects.models import Project
+from readthedocs.projects.models import Project, Domain
class ProjectSerializer(serializers.ModelSerializer):
@@ -70,3 +70,17 @@ class SearchIndexSerializer(serializers.Serializer):
project = serializers.CharField(max_length=500, required=False)
version = serializers.CharField(max_length=500, required=False)
page = serializers.CharField(max_length=500, required=False)
+
+
+class DomainSerializer(serializers.ModelSerializer):
+
+ class Meta:
+ model = Domain
+ fields = (
+ 'id',
+ 'project',
+ 'url',
+ 'canonical',
+ 'active',
+ )
+
diff --git a/readthedocs/templates/projects/project_edit_base.html b/readthedocs/templates/projects/project_edit_base.html
index d0a1229f093..5862356a9af 100644
--- a/readthedocs/templates/projects/project_edit_base.html
+++ b/readthedocs/templates/projects/project_edit_base.html
@@ -17,6 +17,7 @@
{% trans "Settings" %}
{% trans "Advanced Settings" %}
{% trans "Versions" %}
+ {% trans "Domains" %}
{% trans "Maintainers" %}
{% trans "Redirects" %}
{% trans "Translations" %}
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 21a1a880847..511c60249bf 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -14,6 +14,7 @@ celery-haystack==0.7.2
django-guardian==1.3.0
django-extensions==1.3.8
djangorestframework==3.0.4
+django-vanilla-views==1.0.4
pytest-django==2.8.0
requests==2.3.0
From 86d416c20c0fbea40a351b8a72cea71a8884c709 Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:20:15 -0700
Subject: [PATCH 03/40] Add API endpoint for domains
---
readthedocs/restapi/urls.py | 3 ++-
readthedocs/restapi/views/model_views.py | 20 ++++++++++++++++----
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/readthedocs/restapi/urls.py b/readthedocs/restapi/urls.py
index 0d18e6831e3..28bb1fb58ce 100644
--- a/readthedocs/restapi/urls.py
+++ b/readthedocs/restapi/urls.py
@@ -2,7 +2,7 @@
from rest_framework import routers
-from .views.model_views import BuildViewSet, ProjectViewSet, NotificationViewSet, VersionViewSet
+from .views.model_views import BuildViewSet, ProjectViewSet, NotificationViewSet, VersionViewSet, DomainViewSet
from readthedocs.comments.views import CommentViewSet
router = routers.DefaultRouter()
@@ -10,6 +10,7 @@
router.register(r'version', VersionViewSet)
router.register(r'project', ProjectViewSet)
router.register(r'notification', NotificationViewSet)
+router.register(r'domain', DomainViewSet)
router.register(r'comments', CommentViewSet, base_name="comments")
urlpatterns = patterns(
diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py
index 1d083c1484f..dd153f0ee9d 100644
--- a/readthedocs/restapi/views/model_views.py
+++ b/readthedocs/restapi/views/model_views.py
@@ -1,7 +1,6 @@
import logging
from django.shortcuts import get_object_or_404
-from docutils.utils.math.math2html import Link
from rest_framework import decorators, permissions, viewsets, status
from rest_framework.decorators import detail_route
from rest_framework.renderers import JSONPRenderer, JSONRenderer, BrowsableAPIRenderer
@@ -11,13 +10,12 @@
from readthedocs.builds.models import Build, Version
from readthedocs.core.utils import trigger_build
from readthedocs.oauth import utils as oauth_utils
-from readthedocs.builds.constants import STABLE
from readthedocs.projects.filters import ProjectFilter
-from readthedocs.projects.models import Project, EmailHook
+from readthedocs.projects.models import Project, EmailHook, Domain
from readthedocs.projects.version_handling import determine_stable_version
from readthedocs.restapi.permissions import APIPermission
from readthedocs.restapi.permissions import RelatedProjectIsOwner
-from readthedocs.restapi.serializers import BuildSerializer, ProjectSerializer, VersionSerializer
+from readthedocs.restapi.serializers import BuildSerializer, ProjectSerializer, VersionSerializer, DomainSerializer
import readthedocs.restapi.utils as api_utils
log = logging.getLogger(__name__)
@@ -185,3 +183,17 @@ def get_queryset(self):
for the currently authenticated user.
"""
return self.model.objects.api(self.request.user)
+
+
+class DomainViewSet(viewsets.ReadOnlyModelViewSet):
+ permission_classes = (RelatedProjectIsOwner,)
+ renderer_classes = (JSONRenderer, BrowsableAPIRenderer)
+ serializer_class = DomainSerializer
+ model = Domain
+
+ def get_queryset(self):
+ """
+ This view should return a list of all the purchases
+ for the currently authenticated user.
+ """
+ return self.model.objects.api(self.request.user)
From 27fb6b333a91f071bd4e19bfc43eabcd6448234f Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:20:22 -0700
Subject: [PATCH 04/40] Add domain templates
---
.../projects/domain_confirm_delete.html | 23 ++++++++
.../templates/projects/domain_form.html | 49 +++++++++++++++++
.../templates/projects/domain_list.html | 52 +++++++++++++++++++
3 files changed, 124 insertions(+)
create mode 100644 readthedocs/templates/projects/domain_confirm_delete.html
create mode 100644 readthedocs/templates/projects/domain_form.html
create mode 100644 readthedocs/templates/projects/domain_list.html
diff --git a/readthedocs/templates/projects/domain_confirm_delete.html b/readthedocs/templates/projects/domain_confirm_delete.html
new file mode 100644
index 00000000000..3211a7c1934
--- /dev/null
+++ b/readthedocs/templates/projects/domain_confirm_delete.html
@@ -0,0 +1,23 @@
+{% extends "projects/project_edit_base.html" %}
+
+{% load i18n %}
+
+{% block title %}{% trans "Edit Domains" %}{% endblock %}
+
+{% block nav-dashboard %} class="active"{% endblock %}
+
+{% block editing-option-edit-proj %}class="active"{% endblock %}
+
+{% block project-domains-active %}active{% endblock %}
+
+{% block project_edit_content_header %}{% trans "Confirm Delete" %}{% endblock %}
+
+{% block project_edit_content %}
+
+
+{% endblock %}
+
+
diff --git a/readthedocs/templates/projects/domain_form.html b/readthedocs/templates/projects/domain_form.html
new file mode 100644
index 00000000000..172a98be610
--- /dev/null
+++ b/readthedocs/templates/projects/domain_form.html
@@ -0,0 +1,49 @@
+{% extends "projects/project_edit_base.html" %}
+
+{% load i18n %}
+
+{% block title %}{% trans "Edit Domains" %}{% endblock %}
+
+{% block nav-dashboard %} class="active"{% endblock %}
+
+{% block editing-option-edit-proj %}class="active"{% endblock %}
+
+{% block project-domains-active %}active{% endblock %}
+{% block project_edit_content_header %}{% trans "Domains" %}{% endblock %}
+
+{% block project_edit_content %}
+ {% trans "Existing Domains" %}
+
+
+
+ {% trans "Choose which project you would like to add as a domain." %}
+
+
+{% endblock %}
+
+
+{% block footerjs %}
+ $('#id_domain').autocomplete({
+ source: '{% url "search_autocomplete" %}',
+ minLength: 2,
+ open: function(event, ui) {
+ ac_top = $('.ui-autocomplete').css('top');
+ $('.ui-autocomplete').css({'width': '233px', 'top': ac_top + 10 });
+ }
+ });
+
+{% endblock %}
diff --git a/readthedocs/templates/projects/domain_list.html b/readthedocs/templates/projects/domain_list.html
new file mode 100644
index 00000000000..0afcc9cb1cb
--- /dev/null
+++ b/readthedocs/templates/projects/domain_list.html
@@ -0,0 +1,52 @@
+{% extends "projects/project_edit_base.html" %}
+
+{% load i18n %}
+
+{% block title %}{% trans "Edit Domains" %}{% endblock %}
+
+{% block nav-dashboard %} class="active"{% endblock %}
+
+{% block editing-option-edit-proj %}class="active"{% endblock %}
+
+{% block project-domains-active %}active{% endblock %}
+{% block project_edit_content_header %}{% trans "Domains" %}{% endblock %}
+
+{% block project_edit_content %}
+
+ {% trans "This allows you to add domains to your project. This allows them to live in the same namespace in the URLConf for a subdomain or CNAME." %}
+
+
+ {% trans "Existing Domains" %}
+
+
+
+ {% trans "Add new Domain" %}
+
+{% endblock %}
+
+
+{% block footerjs %}
+ $('#id_domain').autocomplete({
+ source: '{% url "search_autocomplete" %}',
+ minLength: 2,
+ open: function(event, ui) {
+ ac_top = $('.ui-autocomplete').css('top');
+ $('.ui-autocomplete').css({'width': '233px', 'top': ac_top + 10 });
+ }
+ });
+
+{% endblock %}
From 09ecf2d2213c435ed40a5ed5e412db1d658d2f79 Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:35:09 -0700
Subject: [PATCH 05/40] Add Domain tracking to middleware & test it
---
readthedocs/core/middleware.py | 12 ++++++-
readthedocs/rtd_tests/tests/test_domains.py | 37 +++++++++++++++++++++
2 files changed, 48 insertions(+), 1 deletion(-)
create mode 100644 readthedocs/rtd_tests/tests/test_domains.py
diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py
index 275d9318802..ca400d6b1fc 100644
--- a/readthedocs/core/middleware.py
+++ b/readthedocs/core/middleware.py
@@ -7,7 +7,7 @@
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.http import Http404
-from readthedocs.projects.models import Project
+from readthedocs.projects.models import Project, Domain
import redis
@@ -71,6 +71,16 @@ def process_request(self, request):
log.debug(LOG_TEMPLATE.format(
msg='CNAME detetected: %s' % request.slug,
**log_kwargs))
+ try:
+ proj = Project.objects.get(slug=slug)
+ domain = Domain.objects.get_or_create(
+ project=proj,
+ url=host,
+ )
+ except (ObjectDoesNotExist, MultipleObjectsReturned):
+ log.debug(LOG_TEMPLATE.format(
+ msg='Project CNAME does not exist: %s' % slug,
+ **log_kwargs))
except:
# Some crazy person is CNAMEing to us. 404.
log.exception(LOG_TEMPLATE.format(msg='CNAME 404', **log_kwargs))
diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py
new file mode 100644
index 00000000000..cf58393a5ba
--- /dev/null
+++ b/readthedocs/rtd_tests/tests/test_domains.py
@@ -0,0 +1,37 @@
+from django.core.cache import cache
+from django.test import TestCase
+from django.test.client import RequestFactory
+from django.test.utils import override_settings
+
+from django_dynamic_fixture import get
+
+from readthedocs.core.middleware import SubdomainMiddleware
+from readthedocs.projects.models import Project, Domain
+
+
+class MiddlewareTests(TestCase):
+
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.middleware = SubdomainMiddleware()
+ self.url = '/'
+
+ @override_settings(PRODUCTION_DOMAIN='readthedocs.org')
+ def test_cname_creation(self):
+ self.assertEqual(Domain.objects.count(), 0)
+ self.project = get(Project, slug='my_slug')
+ cache.get = lambda x: 'my_slug'
+ request = self.factory.get(self.url, HTTP_HOST='my.valid.hostname')
+ self.middleware.process_request(request)
+ self.assertEqual(Domain.objects.count(), 1)
+ self.assertEqual(Domain.objects.first().url, 'my.valid.hostname')
+
+ @override_settings(PRODUCTION_DOMAIN='readthedocs.org')
+ def test_no_readthedocs_domain(self):
+ self.assertEqual(Domain.objects.count(), 0)
+ self.project = get(Project, slug='pip')
+ cache.get = lambda x: 'my_slug'
+ request = self.factory.get(self.url, HTTP_HOST='pip.readthedocs.org')
+ self.middleware.process_request(request)
+ self.assertEqual(Domain.objects.count(), 0)
+
From dc59895d63b57bbcce04c61286b9b7179de897b7 Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:36:40 -0700
Subject: [PATCH 06/40] Fix linting errors
---
readthedocs/restapi/serializers.py | 3 ++-
readthedocs/restapi/urls.py | 4 +++-
readthedocs/restapi/views/model_views.py | 4 +++-
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py
index 8ed9eed3448..b9a92dfa334 100644
--- a/readthedocs/restapi/serializers.py
+++ b/readthedocs/restapi/serializers.py
@@ -19,6 +19,7 @@ class Meta:
class ProjectFullSerializer(ProjectSerializer):
+
'''Serializer for all fields on project model'''
class Meta:
@@ -57,6 +58,7 @@ class Meta:
class VersionFullSerializer(VersionSerializer):
+
'''Serializer for all fields on version model'''
project = ProjectFullSerializer()
@@ -83,4 +85,3 @@ class Meta:
'canonical',
'active',
)
-
diff --git a/readthedocs/restapi/urls.py b/readthedocs/restapi/urls.py
index 28bb1fb58ce..a176f61ff8f 100644
--- a/readthedocs/restapi/urls.py
+++ b/readthedocs/restapi/urls.py
@@ -2,7 +2,9 @@
from rest_framework import routers
-from .views.model_views import BuildViewSet, ProjectViewSet, NotificationViewSet, VersionViewSet, DomainViewSet
+from .views.model_views import (
+ BuildViewSet, ProjectViewSet, NotificationViewSet, VersionViewSet, DomainViewSet
+)
from readthedocs.comments.views import CommentViewSet
router = routers.DefaultRouter()
diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py
index dd153f0ee9d..5819827b0a6 100644
--- a/readthedocs/restapi/views/model_views.py
+++ b/readthedocs/restapi/views/model_views.py
@@ -15,7 +15,9 @@
from readthedocs.projects.version_handling import determine_stable_version
from readthedocs.restapi.permissions import APIPermission
from readthedocs.restapi.permissions import RelatedProjectIsOwner
-from readthedocs.restapi.serializers import BuildSerializer, ProjectSerializer, VersionSerializer, DomainSerializer
+from readthedocs.restapi.serializers import (
+ BuildSerializer, ProjectSerializer, VersionSerializer, DomainSerializer
+)
import readthedocs.restapi.utils as api_utils
log = logging.getLogger(__name__)
From 8aa257fd43450a0393ef344b19beada8915f174e Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 19 Aug 2015 11:42:58 -0700
Subject: [PATCH 07/40] Clean up domain admin a bit
---
readthedocs/projects/forms.py | 8 ++++----
readthedocs/templates/projects/domain_form.html | 4 ++++
readthedocs/templates/projects/domain_list.html | 3 +--
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py
index 22b04d5aeff..873f156144c 100644
--- a/readthedocs/projects/forms.py
+++ b/readthedocs/projects/forms.py
@@ -445,8 +445,8 @@ def __init__(self, *args, **kwargs):
super(DomainForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
- domain = Domain.objects.create(
- project=self.project,
- **self.cleaned_data
- )
+ kwargs['commit'] = False
+ domain = super(DomainForm, self).save(*args, **kwargs)
+ domain.project = self.project
+ domain.save()
return domain
diff --git a/readthedocs/templates/projects/domain_form.html b/readthedocs/templates/projects/domain_form.html
index 172a98be610..956ac1eea57 100644
--- a/readthedocs/templates/projects/domain_form.html
+++ b/readthedocs/templates/projects/domain_form.html
@@ -27,7 +27,11 @@ {% trans "Existing Domains" %}
{% trans "Choose which project you would like to add as a domain." %}
+ {% if domain %}
+
{% endblock %}
-
-{% block footerjs %}
- $('#id_domain').autocomplete({
- source: '{% url "search_autocomplete" %}',
- minLength: 2,
- open: function(event, ui) {
- ac_top = $('.ui-autocomplete').css('top');
- $('.ui-autocomplete').css({'width': '233px', 'top': ac_top + 10 });
- }
- });
-
-{% endblock %}
From 227d5a3ab3ff93e0a10cc5c03b5a2f4b929d8ae2 Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 16 Sep 2015 18:45:09 -0700
Subject: [PATCH 38/40] Clean up project injection on the Domain form
---
readthedocs/projects/forms.py | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py
index 6fbe1c4bae6..ed5a3dda3f0 100644
--- a/readthedocs/projects/forms.py
+++ b/readthedocs/projects/forms.py
@@ -467,15 +467,19 @@ def save(self, **_):
class DomainForm(forms.ModelForm):
+ project = forms.CharField(widget=forms.HiddenInput(), required=False)
class Meta:
model = Domain
- exclude = ['project', 'machine', 'cname', 'count']
+ exclude = ['machine', 'cname', 'count']
def __init__(self, *args, **kwargs):
self.project = kwargs.pop('project', None)
super(DomainForm, self).__init__(*args, **kwargs)
+ def clean_project(self):
+ return self.project
+
def clean_canonical(self):
canonical = self.cleaned_data['canonical']
if canonical and Domain.objects.filter(
@@ -483,10 +487,3 @@ def clean_canonical(self):
).exclude(url=self.cleaned_data['url']).exists():
raise forms.ValidationError(_(u'Only 1 Domain can be canonical at a time.'))
return canonical
-
- def save(self, *args, **kwargs):
- kwargs['commit'] = False
- domain = super(DomainForm, self).save(*args, **kwargs)
- domain.project = self.project
- domain.save()
- return domain
From 00753623436d165f91d04d721a67322b55142dbe Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 16 Sep 2015 18:45:42 -0700
Subject: [PATCH 39/40] Fix logic around Create/Delete of domains
---
.../templates/projects/domain_form.html | 30 ++++++++-----------
1 file changed, 12 insertions(+), 18 deletions(-)
diff --git a/readthedocs/templates/projects/domain_form.html b/readthedocs/templates/projects/domain_form.html
index 3012e7c5853..48a1ac8a1b2 100644
--- a/readthedocs/templates/projects/domain_form.html
+++ b/readthedocs/templates/projects/domain_form.html
@@ -9,28 +9,22 @@
{% block editing-option-edit-proj %}class="active"{% endblock %}
{% block project-domains-active %}active{% endblock %}
-{% block project_edit_content_header %}{% trans "Domains" %}{% endblock %}
+
+{% block project_edit_content_header %}
+
+{% if domain %}
+{% trans "Edit Domain" %}
+{% else %}
+{% trans "Create Domain" %}
+{% endif %}
+
+{% endblock %}
{% block project_edit_content %}
- {% trans "Existing Domains" %}
-
-
-
- {% trans "Choose which project you would like to add as a domain." %}
-
{% if domain %}
- {% url 'projects_domains_create' project.slug as action_url %}
- {% else %}
{% url 'projects_domains_edit' project.slug domain.pk as action_url %}
+ {% else %}
+ {% url 'projects_domains_create' project.slug as action_url %}
{% endif %}
{% csrf_token %}
{{ form.as_p }}
From 6cb9d9d1ea62dff1efad34b1500e7f65f6be4572 Mon Sep 17 00:00:00 2001
From: Eric Holscher
Date: Wed, 16 Sep 2015 18:48:39 -0700
Subject: [PATCH 40/40] CLean up more templates
---
readthedocs/templates/projects/domain_confirm_delete.html | 4 ++--
readthedocs/templates/projects/domain_list.html | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/readthedocs/templates/projects/domain_confirm_delete.html b/readthedocs/templates/projects/domain_confirm_delete.html
index 3211a7c1934..48cd4fc6f94 100644
--- a/readthedocs/templates/projects/domain_confirm_delete.html
+++ b/readthedocs/templates/projects/domain_confirm_delete.html
@@ -14,8 +14,8 @@
{% block project_edit_content %}
{% csrf_token %}
- Are you sure you want to delete "{{ object }}"?
-
+ Are you sure you want to delete {{ object.url }} ?
+
{% endblock %}
diff --git a/readthedocs/templates/projects/domain_list.html b/readthedocs/templates/projects/domain_list.html
index eb012bb2aa5..bd08e713ce0 100644
--- a/readthedocs/templates/projects/domain_list.html
+++ b/readthedocs/templates/projects/domain_list.html
@@ -16,6 +16,7 @@
{% trans "This allows you to add domains to your project. This allows them to live in the same namespace in the URLConf for a subdomain or CNAME." %}
+ {% if object_list %}
{% trans "Existing Domains" %}
@@ -30,6 +31,8 @@ {% trans "Existing Domains" %}
{% endfor %}
+ {% endif %}
+
{% trans "Add new Domain" %}
{% csrf_token %}
{{ form.as_p }}