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 %} +
    {% csrf_token %} +

    Are you sure you want to delete "{{ object }}"?

    + +
    + +{% 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." %} +

    +
    {% csrf_token %} + {{ form.as_p }} +

    + +

    +
    +{% 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" %}

    +
    {% csrf_token %} + {{ form.as_p }} +

    + +

    +
    +{% 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 %} +
    {% csrf_token %} + {% else %} {% csrf_token %} + {% endif %} {{ form.as_p }}

    diff --git a/readthedocs/templates/projects/domain_list.html b/readthedocs/templates/projects/domain_list.html index 0afcc9cb1cb..d8c871be849 100644 --- a/readthedocs/templates/projects/domain_list.html +++ b/readthedocs/templates/projects/domain_list.html @@ -21,9 +21,8 @@

    {% trans "Existing Domains" %}

      {% for domain in object_list %}
    • - {{ domain }} - + ({% trans "Edit" %}) ({% trans "Remove" %})
    • {% endfor %} From 3669f48053992916934fe7eb0fe9d89712f484e6 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 11:57:00 -0700 Subject: [PATCH 08/40] Clean up tests --- readthedocs/rtd_tests/tests/test_domains.py | 4 ++++ readthedocs/rtd_tests/tests/test_middleware.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index cf58393a5ba..686c7a60f3f 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -15,6 +15,10 @@ def setUp(self): self.factory = RequestFactory() self.middleware = SubdomainMiddleware() self.url = '/' + self.old_cache_get = cache.get + + def tearDown(self): + cache.get = self.old_cache_get @override_settings(PRODUCTION_DOMAIN='readthedocs.org') def test_cname_creation(self): diff --git a/readthedocs/rtd_tests/tests/test_middleware.py b/readthedocs/rtd_tests/tests/test_middleware.py index c56ee22769c..39321877912 100644 --- a/readthedocs/rtd_tests/tests/test_middleware.py +++ b/readthedocs/rtd_tests/tests/test_middleware.py @@ -1,13 +1,13 @@ from django.http import Http404 from django.core.cache import cache -from django.utils import unittest +from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from readthedocs.core.middleware import SubdomainMiddleware -class MiddlewareTests(unittest.TestCase): +class MiddlewareTests(TestCase): def setUp(self): self.factory = RequestFactory() From d1e7a6f60ba3f67e4555b9501df2d53b38bd656a Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:08:31 -0700 Subject: [PATCH 09/40] Add nicer UX and check for only 1 active canonical model --- readthedocs/projects/forms.py | 8 ++++++++ readthedocs/projects/models.py | 3 +++ readthedocs/templates/projects/domain_list.html | 6 ++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 873f156144c..d5e42319358 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -444,6 +444,14 @@ def __init__(self, *args, **kwargs): self.project = kwargs.pop('project', None) super(DomainForm, self).__init__(*args, **kwargs) + def clean_canonical(self): + canonical = self.cleaned_data.get('canonical', False) + if canonical and Domain.objects.filter( + project=self.project, canonical=True + ).exclude(project__pk=self.project.pk).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) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 593580de89b..126eb5a8d9b 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -911,5 +911,8 @@ class Domain(models.Model): _('Active'), default=False, help_text=_('This is an active domain for this project.')) + class Meta: + ordering = ('-canonical', '-active', 'url') + def __unicode__(self): return "{url} pointed at {project}".format(url=self.url, project=self.project.name) diff --git a/readthedocs/templates/projects/domain_list.html b/readthedocs/templates/projects/domain_list.html index d8c871be849..0f3ceba1234 100644 --- a/readthedocs/templates/projects/domain_list.html +++ b/readthedocs/templates/projects/domain_list.html @@ -20,8 +20,10 @@

      {% trans "Existing Domains" %}

        {% for domain in object_list %} -
      • - {{ domain }} +
      • + {% if domain.canonical %}{% endif %} + {{ domain.url }} + {% if domain.canonical %}{% endif %} ({% trans "Edit" %}) ({% trans "Remove" %})
      • From 6b6eaa286b90c9b5dbaaaa78f0ce6fcff7050aab Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:14:19 -0700 Subject: [PATCH 10/40] Add data migration --- .../migrations/0005_migrate_canonical_data.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 readthedocs/projects/migrations/0005_migrate_canonical_data.py diff --git a/readthedocs/projects/migrations/0005_migrate_canonical_data.py b/readthedocs/projects/migrations/0005_migrate_canonical_data.py new file mode 100644 index 00000000000..11b4db9b977 --- /dev/null +++ b/readthedocs/projects/migrations/0005_migrate_canonical_data.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def migrate_canonical(apps, schema_editor): + Project = apps.get_model("projects", "Project") + for project in Project.objects.all(): + if project.canonical_url: + domain = project.domains.create( + url=project.canonical_url, + canonical=True, + active=True, + ) + print "Added {url} to {project}".format(url=domain.url, project=project.name) + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0004_add_cname_modeling'), + ] + + operations = [ + migrations.RunPython(migrate_canonical) + ] From 0c66e476befd539cac3b455af70a7b28692a6fdc Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:14:25 -0700 Subject: [PATCH 11/40] Add Domain inline for Project admin --- readthedocs/projects/admin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/readthedocs/projects/admin.py b/readthedocs/projects/admin.py index f9a0cd44cd7..0b56c7dbd73 100644 --- a/readthedocs/projects/admin.py +++ b/readthedocs/projects/admin.py @@ -24,13 +24,17 @@ class RedirectInline(admin.TabularInline): model = Redirect +class DomainInline(admin.TabularInline): + model = Domain + + class ProjectAdmin(GuardedModelAdmin): prepopulated_fields = {'slug': ('name',)} list_display = ('name', 'repo', 'repo_type', 'allow_comments', 'featured', 'theme') list_filter = ('repo_type', 'allow_comments', 'featured', 'privacy_level', 'documentation_type', 'programming_language') list_editable = ('featured',) search_fields = ('slug', 'repo') - inlines = [ProjectRelationshipInline, RedirectInline, VersionInline] + inlines = [ProjectRelationshipInline, RedirectInline, VersionInline, DomainInline] raw_id_fields = ('users', 'main_language_project') From bcfe13a70a5349adbe830d7f99241d93a688ce56 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:17:40 -0700 Subject: [PATCH 12/40] Remove canonical_url usage in Project forms --- readthedocs/projects/forms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index d5e42319358..d6c24c6cfb0 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -114,13 +114,13 @@ class Meta: 'documentation_type', 'language', 'programming_language', 'project_url', - 'canonical_url', + #'canonical_url', 'tags', ) def __init__(self, *args, **kwargs): super(ProjectExtraForm, self).__init__(*args, **kwargs) - self.fields['canonical_url'].widget.attrs['placeholder'] = self.placehold_canonical_url() + #self.fields['canonical_url'].widget.attrs['placeholder'] = self.placehold_canonical_url() def placehold_canonical_url(self): return choice([ @@ -177,13 +177,13 @@ class Meta: # Basics 'name', 'repo', 'repo_type', # Extra - #'allow_comments', - #'comment_moderation', + # 'allow_comments', + # 'comment_moderation', 'description', 'documentation_type', 'language', 'programming_language', 'project_url', - 'canonical_url', + # 'canonical_url', 'tags', ) From 318457d727eece813cc1dd0b5037b6df89734ec5 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:26:59 -0700 Subject: [PATCH 13/40] Use Domain objects for CNAME symlinking --- readthedocs/projects/symlinks.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/readthedocs/projects/symlinks.py b/readthedocs/projects/symlinks.py index 90393cbda51..b4cea786b34 100644 --- a/readthedocs/projects/symlinks.py +++ b/readthedocs/projects/symlinks.py @@ -6,6 +6,7 @@ from readthedocs.core.utils import run_on_app_servers from readthedocs.projects.constants import LOG_TEMPLATE +from readthedocs.projects.models import Domain from readthedocs.restapi.client import api log = logging.getLogger(__name__) @@ -20,24 +21,19 @@ def symlink_cnames(version): Link from HOME/user_builds/cnametoproject/ -> HOME/user_builds// """ - try: - redis_conn = redis.Redis(**settings.REDIS) - cnames = redis_conn.smembers('rtd_slug:v1:%s' % version.project.slug) - except redis.ConnectionError: - log.error(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg='Failed to symlink cnames, Redis error.'), exc_info=True) - return - for cname in cnames: - log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg="Symlinking CNAME: %s" % cname)) + domains = Domain.objects.filter(project=version.project, active=True) + for domain in domains: + log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg="Symlinking CNAME: %s" % domain.url)) docs_dir = version.project.rtd_build_path(version.slug) # Chop off the version from the end. docs_dir = '/'.join(docs_dir.split('/')[:-1]) # Old symlink location -- Keep this here til we change nginx over - symlink = version.project.cnames_symlink_path(cname) + symlink = version.project.cnames_symlink_path(domain.url) run_on_app_servers('mkdir -p %s' % '/'.join(symlink.split('/')[:-1])) run_on_app_servers('ln -nsf %s %s' % (docs_dir, symlink)) # New symlink location new_docs_dir = version.project.doc_path - new_cname_symlink = os.path.join(getattr(settings, 'SITE_ROOT'), 'cnametoproject', cname) + new_cname_symlink = os.path.join(getattr(settings, 'SITE_ROOT'), 'cnametoproject', domain.url) run_on_app_servers('mkdir -p %s' % '/'.join(new_cname_symlink.split('/')[:-1])) run_on_app_servers('ln -nsf %s %s' % (new_docs_dir, new_cname_symlink)) From cec9706672a9e24cc1db772deaaf16691a05801d Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:47:02 -0700 Subject: [PATCH 14/40] Add tests for symlinking code --- .../rtd_tests/tests/test_project_symlinks.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/readthedocs/rtd_tests/tests/test_project_symlinks.py b/readthedocs/rtd_tests/tests/test_project_symlinks.py index 2227c836db4..dedd2e0ae80 100644 --- a/readthedocs/rtd_tests/tests/test_project_symlinks.py +++ b/readthedocs/rtd_tests/tests/test_project_symlinks.py @@ -2,11 +2,13 @@ from functools import wraps from mock import patch +from django.conf import settings from django.test import TestCase +from django_dynamic_fixture import get from readthedocs.builds.models import Version -from readthedocs.projects.models import Project -from readthedocs.projects.symlinks import symlink_translations +from readthedocs.projects.models import Project, Domain +from readthedocs.projects.symlinks import symlink_translations, symlink_cnames def patched(fn): @@ -24,23 +26,52 @@ def _collect_commands(cmd): return wrapper +class TestSymlinkCnames(TestCase): + + def setUp(self): + self.project = get(Project, slug='kong') + self.version = get(Version, verbose_name='latest', active=True, project=self.project) + self.args = { + 'project': self.project.doc_path, + 'cnames_root': settings.CNAME_ROOT, + 'new_cnames_root': os.path.join(getattr(settings, 'SITE_ROOT'), 'cnametoproject'), + 'build_path': self.project.doc_path, + } + self.commands = [] + + @patched + def test_symlink_cname(self): + self.cname = get(Domain, project=self.project, url='http://woot.com', active=True) + symlink_cnames(self.project.versions.first()) + self.args['cname'] = self.cname.url + commands = [ + 'mkdir -p {cnames_root}', + 'ln -nsf {build_path}/rtd-builds {cnames_root}/{cname}', + 'mkdir -p {new_cnames_root}', + 'ln -nsf {build_path} {new_cnames_root}/{cname}' + ] + + for command in commands: + self.assertIsNotNone( + self.commands.pop( + self.commands.index(command.format(**self.args)) + )) + + class TestSymlinkTranslations(TestCase): - fixtures = ['eric', 'test_data'] commands = [] def setUp(self): - self.project = Project.objects.get(slug='kong') - self.translation = Project.objects.get(slug='pip') + self.project = get(Project, slug='kong') + self.translation = get(Project, slug='pip') self.translation.language = 'de' self.translation.main_lanuage_project = self.project self.project.translations.add(self.translation) self.translation.save() self.project.save() - Version.objects.create(verbose_name='master', - active=True, project=self.project) - Version.objects.create(verbose_name='master', - active=True, project=self.translation) + get(Version, verbose_name='master', active=True, project=self.project) + get(Version, verbose_name='master', active=True, project=self.translation) self.args = { 'project': self.project.doc_path, 'translation': self.translation.doc_path, From a594e6321419085b7861c636b072b736756d93d9 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 12:47:33 -0700 Subject: [PATCH 15/40] Make domains nicely cleaned before save --- readthedocs/projects/models.py | 11 +++++++++++ readthedocs/projects/symlinks.py | 1 - 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 126eb5a8d9b..5521f3d53e0 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -916,3 +916,14 @@ class Meta: def __unicode__(self): return "{url} pointed at {project}".format(url=self.url, project=self.project.name) + + def save(self, *args, **kwargs): + first_save = self.pk is None + if first_save: + parsed = urlparse(self.url) + if parsed.scheme or parsed.netloc: + netloc = parsed.netloc + else: + netloc = parsed.path + self.url = netloc + super(Domain, self).save(*args, **kwargs) diff --git a/readthedocs/projects/symlinks.py b/readthedocs/projects/symlinks.py index b4cea786b34..3107da9c7ce 100644 --- a/readthedocs/projects/symlinks.py +++ b/readthedocs/projects/symlinks.py @@ -2,7 +2,6 @@ import logging from django.conf import settings -import redis from readthedocs.core.utils import run_on_app_servers from readthedocs.projects.constants import LOG_TEMPLATE From 9991b77ea6eecc2bf9391661dd9c1748b0d4111d Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 13:08:19 -0700 Subject: [PATCH 16/40] Add domain model tests --- readthedocs/rtd_tests/tests/test_domains.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index 686c7a60f3f..b51b7a273a2 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -39,3 +39,24 @@ def test_no_readthedocs_domain(self): self.middleware.process_request(request) self.assertEqual(Domain.objects.count(), 0) + +class ModelTests(TestCase): + + def setUp(self): + self.project = get(Project, slug='kong') + + def test_save_parsing(self): + domain = get(Domain, url='http://google.com') + self.assertEqual(domain.url, 'google.com') + + domain.url = 'google.com' + domain.save() + self.assertEqual(domain.url, 'google.com') + + domain.url = 'https://google.com' + domain.save() + self.assertEqual(domain.url, 'google.com') + + domain.url = 'www.google.com' + domain.save() + self.assertEqual(domain.url, 'www.google.com') From da2c644ae26e03cad90caec637b9b0f0c11a11ee Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 14:09:22 -0700 Subject: [PATCH 17/40] Clean up a bit of the modeling & use basic count --- readthedocs/core/middleware.py | 8 ++++- readthedocs/projects/forms.py | 2 +- .../migrations/0004_add_cname_modeling.py | 12 +++++-- readthedocs/projects/models.py | 35 +++++++++++-------- readthedocs/projects/symlinks.py | 2 +- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index ca400d6b1fc..ea31c2b93f0 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) LOG_TEMPLATE = u"(Middleware) {msg} [{host}{path}]" - +MAX_DOMAIN_COUNT = 5000 class SubdomainMiddleware(object): @@ -76,7 +76,13 @@ def process_request(self, request): domain = Domain.objects.get_or_create( project=proj, url=host, + machine=True, + cname=True, ) + if domain.count <= MAX_DOMAIN_COUNT: + domain.count = domain.count + 1 + domain.save() + except (ObjectDoesNotExist, MultipleObjectsReturned): log.debug(LOG_TEMPLATE.format( msg='Project CNAME does not exist: %s' % slug, diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index d6c24c6cfb0..323f492837b 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -438,7 +438,7 @@ class DomainForm(forms.ModelForm): class Meta: model = Domain - exclude = ['project'] + exclude = ['project', 'machine'] def __init__(self, *args, **kwargs): self.project = kwargs.pop('project', None) diff --git a/readthedocs/projects/migrations/0004_add_cname_modeling.py b/readthedocs/projects/migrations/0004_add_cname_modeling.py index d5b0f0f9924..dafd77da52c 100644 --- a/readthedocs/projects/migrations/0004_add_cname_modeling.py +++ b/readthedocs/projects/migrations/0004_add_cname_modeling.py @@ -15,10 +15,16 @@ class Migration(migrations.Migration): 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')), + ('url', models.URLField(unique=True, verbose_name='URL')), + ('machine', models.BooleanField(default=False, help_text='This URL was auto-created')), + ('cname', models.BooleanField(default=False, help_text='This URL is a CNAME for the project')), + ('canonical', models.BooleanField(default=False, help_text='This URL is the primary one where the documentation is served from.')), + ('active', models.BooleanField(default=False, help_text='This is an active domain for this project.')), + ('count', models.IntegerField(help_text='Number of times this domain has been hit.')), ], + options={ + 'ordering': ('-canonical', '-active', 'url'), + }, ), migrations.AlterField( model_name='project', diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 5521f3d53e0..efed1e1b61f 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -905,11 +905,21 @@ class Domain(models.Model): 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.')) + + machine = models.BooleanField( + default=False, help_text=_('This URL was auto-created') + ) + cname = models.BooleanField( + default=False, help_text=_('This URL is a CNAME for the project') + ) + canonical = models.BooleanField( + default=False, help_text=_('This URL is the primary one where the documentation is served from.') + ) active = models.BooleanField( - _('Active'), default=False, - help_text=_('This is an active domain for this project.')) + default=False, help_text=_('This is an active domain for this project.') + ) + + count = models.IntegerField(help_text=_('Number of times this domain has been hit.')) class Meta: ordering = ('-canonical', '-active', 'url') @@ -917,13 +927,10 @@ class Meta: def __unicode__(self): return "{url} pointed at {project}".format(url=self.url, project=self.project.name) - def save(self, *args, **kwargs): - first_save = self.pk is None - if first_save: - parsed = urlparse(self.url) - if parsed.scheme or parsed.netloc: - netloc = parsed.netloc - else: - netloc = parsed.path - self.url = netloc - super(Domain, self).save(*args, **kwargs) + @property + def clean_host(self): + parsed = urlparse(self.url) + if parsed.scheme or parsed.netloc: + return parsed.netloc + else: + return parsed.path diff --git a/readthedocs/projects/symlinks.py b/readthedocs/projects/symlinks.py index 3107da9c7ce..2e1c8a00916 100644 --- a/readthedocs/projects/symlinks.py +++ b/readthedocs/projects/symlinks.py @@ -20,7 +20,7 @@ def symlink_cnames(version): Link from HOME/user_builds/cnametoproject/ -> HOME/user_builds// """ - domains = Domain.objects.filter(project=version.project, active=True) + domains = Domain.objects.filter(project=version.project, active=True, cname=True) for domain in domains: log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg="Symlinking CNAME: %s" % domain.url)) docs_dir = version.project.rtd_build_path(version.slug) From a9e512f043900f776dbdd6174a366158214d34c9 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 14:15:51 -0700 Subject: [PATCH 18/40] Fix tests & logic --- readthedocs/core/middleware.py | 2 +- readthedocs/projects/models.py | 2 +- readthedocs/rtd_tests/tests/test_domains.py | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index ea31c2b93f0..528abe62acf 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -73,7 +73,7 @@ def process_request(self, request): **log_kwargs)) try: proj = Project.objects.get(slug=slug) - domain = Domain.objects.get_or_create( + domain, created = Domain.objects.get_or_create( project=proj, url=host, machine=True, diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index efed1e1b61f..9fe1eb90329 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -919,7 +919,7 @@ class Domain(models.Model): default=False, help_text=_('This is an active domain for this project.') ) - count = models.IntegerField(help_text=_('Number of times this domain has been hit.')) + count = models.IntegerField(default=0, help_text=_('Number of times this domain has been hit.')) class Meta: ordering = ('-canonical', '-active', 'url') diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index b51b7a273a2..ee8a8229df8 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -39,6 +39,22 @@ def test_no_readthedocs_domain(self): self.middleware.process_request(request) self.assertEqual(Domain.objects.count(), 0) + @override_settings(PRODUCTION_DOMAIN='readthedocs.org') + def test_cname_count(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') + self.assertEqual(Domain.objects.first().count, 1) + + self.middleware.process_request(request) + self.assertEqual(Domain.objects.count(), 1) + self.assertEqual(Domain.objects.first().count, 2) + class ModelTests(TestCase): @@ -47,7 +63,7 @@ def setUp(self): def test_save_parsing(self): domain = get(Domain, url='http://google.com') - self.assertEqual(domain.url, 'google.com') + self.assertEqual(domain.url, 'http://google.com') domain.url = 'google.com' domain.save() @@ -55,7 +71,7 @@ def test_save_parsing(self): domain.url = 'https://google.com' domain.save() - self.assertEqual(domain.url, 'google.com') + self.assertEqual(domain.url, 'https://google.com') domain.url = 'www.google.com' domain.save() From 78e849769fe84ab3903f8f72b0b2424d76b416c2 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 14:18:53 -0700 Subject: [PATCH 19/40] Use Domain modeling for canonical setup --- readthedocs/projects/models.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 9fe1eb90329..95cb4dbf745 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -5,6 +5,7 @@ from django.conf import settings from django.contrib.auth.models import User +from django.core.exceptions import MultipleObjectsReturned from django.core.urlresolvers import reverse from django.db import models from django.template.defaultfilters import slugify @@ -413,21 +414,27 @@ def canonical_domain(self): @property def clean_canonical_url(self): - if not self.canonical_url: - return "" - parsed = urlparse(self.canonical_url) + try: + domain = self.domains.get(canonical=True) + except (Domain.DoesNotExist, MultipleObjectsReturned): + return '' + + parsed = urlparse(domain.url) + if parsed.scheme: scheme, netloc = parsed.scheme, parsed.netloc elif parsed.netloc: scheme, netloc = "http", parsed.netloc else: scheme, netloc = "http", parsed.path + if getattr(settings, 'DONT_HIT_DB', True): if parsed.path: netloc = netloc + parsed.path else: if self.superprojects.count() and parsed.path: netloc = netloc + parsed.path + return "%s://%s/" % (scheme, netloc) @property From 0d9a5c9796a424fd95da7af06bdc6056a4a2a88f Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 14:26:05 -0700 Subject: [PATCH 20/40] Clean up symlinking and symlink testing --- readthedocs/core/middleware.py | 2 ++ readthedocs/projects/symlinks.py | 8 ++++---- readthedocs/rtd_tests/tests/test_domains.py | 9 ++++----- readthedocs/rtd_tests/tests/test_project_symlinks.py | 11 ++++------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 528abe62acf..de98838c3bc 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -16,6 +16,7 @@ LOG_TEMPLATE = u"(Middleware) {msg} [{host}{path}]" MAX_DOMAIN_COUNT = 5000 + class SubdomainMiddleware(object): def process_request(self, request): @@ -79,6 +80,7 @@ def process_request(self, request): machine=True, cname=True, ) + # Track basic domain counts so we know which are heavily used if domain.count <= MAX_DOMAIN_COUNT: domain.count = domain.count + 1 domain.save() diff --git a/readthedocs/projects/symlinks.py b/readthedocs/projects/symlinks.py index 2e1c8a00916..bd558bf73b6 100644 --- a/readthedocs/projects/symlinks.py +++ b/readthedocs/projects/symlinks.py @@ -20,19 +20,19 @@ def symlink_cnames(version): Link from HOME/user_builds/cnametoproject/ -> HOME/user_builds// """ - domains = Domain.objects.filter(project=version.project, active=True, cname=True) + domains = Domain.objects.filter(project=version.project, cname=True) for domain in domains: - log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg="Symlinking CNAME: %s" % domain.url)) + log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg="Symlinking CNAME: %s" % domain.clean_host)) docs_dir = version.project.rtd_build_path(version.slug) # Chop off the version from the end. docs_dir = '/'.join(docs_dir.split('/')[:-1]) # Old symlink location -- Keep this here til we change nginx over - symlink = version.project.cnames_symlink_path(domain.url) + symlink = version.project.cnames_symlink_path(domain.clean_host) run_on_app_servers('mkdir -p %s' % '/'.join(symlink.split('/')[:-1])) run_on_app_servers('ln -nsf %s %s' % (docs_dir, symlink)) # New symlink location new_docs_dir = version.project.doc_path - new_cname_symlink = os.path.join(getattr(settings, 'SITE_ROOT'), 'cnametoproject', domain.url) + new_cname_symlink = os.path.join(getattr(settings, 'SITE_ROOT'), 'cnametoproject', domain.clean_host) run_on_app_servers('mkdir -p %s' % '/'.join(new_cname_symlink.split('/')[:-1])) run_on_app_servers('ln -nsf %s %s' % (new_docs_dir, new_cname_symlink)) diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index ee8a8229df8..7e0e6c0bc61 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -63,16 +63,15 @@ def setUp(self): def test_save_parsing(self): domain = get(Domain, url='http://google.com') - self.assertEqual(domain.url, 'http://google.com') + self.assertEqual(domain.clean_host, 'google.com') domain.url = 'google.com' - domain.save() - self.assertEqual(domain.url, 'google.com') + self.assertEqual(domain.clean_host, 'google.com') domain.url = 'https://google.com' domain.save() - self.assertEqual(domain.url, 'https://google.com') + self.assertEqual(domain.clean_host, 'google.com') domain.url = 'www.google.com' domain.save() - self.assertEqual(domain.url, 'www.google.com') + self.assertEqual(domain.clean_host, 'www.google.com') diff --git a/readthedocs/rtd_tests/tests/test_project_symlinks.py b/readthedocs/rtd_tests/tests/test_project_symlinks.py index dedd2e0ae80..e878363c893 100644 --- a/readthedocs/rtd_tests/tests/test_project_symlinks.py +++ b/readthedocs/rtd_tests/tests/test_project_symlinks.py @@ -41,9 +41,9 @@ def setUp(self): @patched def test_symlink_cname(self): - self.cname = get(Domain, project=self.project, url='http://woot.com', active=True) + self.cname = get(Domain, project=self.project, url='http://woot.com', cname=True) symlink_cnames(self.project.versions.first()) - self.args['cname'] = self.cname.url + self.args['cname'] = self.cname.clean_host commands = [ 'mkdir -p {cnames_root}', 'ln -nsf {build_path}/rtd-builds {cnames_root}/{cname}', @@ -51,11 +51,8 @@ def test_symlink_cname(self): 'ln -nsf {build_path} {new_cnames_root}/{cname}' ] - for command in commands: - self.assertIsNotNone( - self.commands.pop( - self.commands.index(command.format(**self.args)) - )) + for index, command in enumerate(commands): + self.assertEqual(self.commands[index], command.format(**self.args)) class TestSymlinkTranslations(TestCase): From add150788372f3a47202ff6e5b8e7250d5283c6f Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 14:36:22 -0700 Subject: [PATCH 21/40] Clean up canonical tests for new scheme --- readthedocs/rtd_tests/tests/test_canonical.py | 39 ------------------- readthedocs/rtd_tests/tests/test_domains.py | 36 +++++++++++++++++ 2 files changed, 36 insertions(+), 39 deletions(-) delete mode 100644 readthedocs/rtd_tests/tests/test_canonical.py diff --git a/readthedocs/rtd_tests/tests/test_canonical.py b/readthedocs/rtd_tests/tests/test_canonical.py deleted file mode 100644 index 8b754b8bd74..00000000000 --- a/readthedocs/rtd_tests/tests/test_canonical.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.test import TestCase -from readthedocs.projects.models import Project - - -class TestCanonical(TestCase): - - def setUp(self): - self.p = Project( - name='foo', - repo='http://github.com/ericholscher/django-kong', - ) - self.p.save() - - - def test_canonical_clean(self): - # Only a url - self.p.canonical_url = "djangokong.com" - self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") - # Extra bits in the URL - self.p.canonical_url = "http://djangokong.com/en/latest/" - self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") - self.p.canonical_url = "http://djangokong.com//" - self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") - # Subdomain - self.p.canonical_url = "foo.djangokong.com" - self.assertEqual(self.p.clean_canonical_url, "http://foo.djangokong.com/") - # Https - self.p.canonical_url = "https://djangokong.com//" - self.assertEqual(self.p.clean_canonical_url, "https://djangokong.com/") - self.p.canonical_url = "https://foo.djangokong.com//" - self.assertEqual(self.p.clean_canonical_url, "https://foo.djangokong.com/") - - """ - # Turn this feature off for now, until we fix the UI. - def test_canonical_subdomain(self): - self.p.canonical_url = "https://djangokong.com//" - with self.settings(USE_SUBDOMAIN=True): - self.assertEqual(self.p.get_docs_url(), "http://djangokong.com/en/latest/") - """ diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index 7e0e6c0bc61..aaa72ba02e0 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -75,3 +75,39 @@ def test_save_parsing(self): domain.url = 'www.google.com' domain.save() self.assertEqual(domain.clean_host, 'www.google.com') + + +class TestCanonical(TestCase): + + def setUp(self): + self.p = Project( + name='foo', + repo='http://github.com/ericholscher/django-kong', + ) + self.p.save() + self.domain = self.p.domains.create(url='djangokong.com', canonical=True) + + def test_canonical_clean(self): + # Only a url + self.p.canonical_url = "djangokong.com" + self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") + # Extra bits in the URL + self.domain.url = "http://djangokong.com/en/latest/" + self.domain.save() + self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") + + self.domain.url = "http://djangokong.com//" + self.domain.save() + self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") + # Subdomain + self.domain.url = "foo.djangokong.com" + self.domain.save() + self.assertEqual(self.p.clean_canonical_url, "http://foo.djangokong.com/") + # Https + self.domain.url = "https://djangokong.com//" + self.domain.save() + self.assertEqual(self.p.clean_canonical_url, "https://djangokong.com/") + + self.domain.url = "https://foo.djangokong.com//" + self.domain.save() + self.assertEqual(self.p.clean_canonical_url, "https://foo.djangokong.com/") From 562088959c4170ef728b4f6bf3fa48c012d643e3 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 14:54:09 -0700 Subject: [PATCH 22/40] =?UTF-8?q?Remove=20active=20concept,=20and=20just?= =?UTF-8?q?=20have=20folks=20delete=20ones=20they=20don=E2=80=99t=20want.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readthedocs/core/middleware.py | 12 +++++------- readthedocs/projects/forms.py | 2 +- .../projects/migrations/0004_add_cname_modeling.py | 5 ++--- .../migrations/0005_migrate_canonical_data.py | 1 - readthedocs/projects/models.py | 5 +---- readthedocs/templates/projects/domain_list.html | 2 +- 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index de98838c3bc..f5b28f22e11 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -14,7 +14,6 @@ log = logging.getLogger(__name__) LOG_TEMPLATE = u"(Middleware) {msg} [{host}{path}]" -MAX_DOMAIN_COUNT = 5000 class SubdomainMiddleware(object): @@ -77,14 +76,13 @@ def process_request(self, request): domain, created = Domain.objects.get_or_create( project=proj, url=host, - machine=True, - cname=True, ) + if created: + domain.machine = True + domain.cname = True # Track basic domain counts so we know which are heavily used - if domain.count <= MAX_DOMAIN_COUNT: - domain.count = domain.count + 1 - domain.save() - + domain.count = domain.count + 1 + domain.save() except (ObjectDoesNotExist, MultipleObjectsReturned): log.debug(LOG_TEMPLATE.format( msg='Project CNAME does not exist: %s' % slug, diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 323f492837b..57ddb12d0e5 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -438,7 +438,7 @@ class DomainForm(forms.ModelForm): class Meta: model = Domain - exclude = ['project', 'machine'] + exclude = ['project', 'machine', 'cname', 'count'] def __init__(self, *args, **kwargs): self.project = kwargs.pop('project', None) diff --git a/readthedocs/projects/migrations/0004_add_cname_modeling.py b/readthedocs/projects/migrations/0004_add_cname_modeling.py index dafd77da52c..2d530588e51 100644 --- a/readthedocs/projects/migrations/0004_add_cname_modeling.py +++ b/readthedocs/projects/migrations/0004_add_cname_modeling.py @@ -19,11 +19,10 @@ class Migration(migrations.Migration): ('machine', models.BooleanField(default=False, help_text='This URL was auto-created')), ('cname', models.BooleanField(default=False, help_text='This URL is a CNAME for the project')), ('canonical', models.BooleanField(default=False, help_text='This URL is the primary one where the documentation is served from.')), - ('active', models.BooleanField(default=False, help_text='This is an active domain for this project.')), - ('count', models.IntegerField(help_text='Number of times this domain has been hit.')), + ('count', models.IntegerField(default=0, help_text='Number of times this domain has been hit.')), ], options={ - 'ordering': ('-canonical', '-active', 'url'), + 'ordering': ('-canonical', '-machine', 'url'), }, ), migrations.AlterField( diff --git a/readthedocs/projects/migrations/0005_migrate_canonical_data.py b/readthedocs/projects/migrations/0005_migrate_canonical_data.py index 11b4db9b977..af8a9075fdb 100644 --- a/readthedocs/projects/migrations/0005_migrate_canonical_data.py +++ b/readthedocs/projects/migrations/0005_migrate_canonical_data.py @@ -11,7 +11,6 @@ def migrate_canonical(apps, schema_editor): domain = project.domains.create( url=project.canonical_url, canonical=True, - active=True, ) print "Added {url} to {project}".format(url=domain.url, project=project.name) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 95cb4dbf745..71d8605e6f9 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -922,14 +922,11 @@ class Domain(models.Model): canonical = models.BooleanField( default=False, help_text=_('This URL is the primary one where the documentation is served from.') ) - active = models.BooleanField( - default=False, help_text=_('This is an active domain for this project.') - ) count = models.IntegerField(default=0, help_text=_('Number of times this domain has been hit.')) class Meta: - ordering = ('-canonical', '-active', 'url') + ordering = ('-canonical', '-machine', 'url') def __unicode__(self): return "{url} pointed at {project}".format(url=self.url, project=self.project.name) diff --git a/readthedocs/templates/projects/domain_list.html b/readthedocs/templates/projects/domain_list.html index 0f3ceba1234..cfcd7a92563 100644 --- a/readthedocs/templates/projects/domain_list.html +++ b/readthedocs/templates/projects/domain_list.html @@ -20,7 +20,7 @@

        {% trans "Existing Domains" %}

          {% for domain in object_list %} -
        • +
        • {% if domain.canonical %}{% endif %} {{ domain.url }} {% if domain.canonical %}{% endif %} From cb7d4c611f2590d02adfbd96aa34bd00b377c43a Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 15:15:59 -0700 Subject: [PATCH 23/40] =?UTF-8?q?Add=20basic=20test=20case=20for=20API,=20?= =?UTF-8?q?to=20verify=20it=20doesn=E2=80=99t=20break?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readthedocs/restapi/serializers.py | 3 ++- readthedocs/rtd_tests/tests/test_domains.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index b9a92dfa334..d264aee8b82 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -83,5 +83,6 @@ class Meta: 'project', 'url', 'canonical', - 'active', + 'machine', + 'cname', ) diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index aaa72ba02e0..8b8336c1694 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -1,3 +1,5 @@ +import json + from django.core.cache import cache from django.test import TestCase from django.test.client import RequestFactory @@ -111,3 +113,16 @@ def test_canonical_clean(self): self.domain.url = "https://foo.djangokong.com//" self.domain.save() self.assertEqual(self.p.clean_canonical_url, "https://foo.djangokong.com/") + + +class TestAPI(TestCase): + + def setUp(self): + self.project = get(Project) + self.domain = self.project.domains.create(url='djangokong.com', canonical=True) + + def test_basic_api(self): + resp = self.client.get('/api/v2/domain/') + self.assertEqual(resp.status_code, 200) + obj = json.loads(resp.content) + self.assertEqual(obj['results'][0]['url'], 'djangokong.com') From 116f18bc67b1212305a6c5478225a8969caf4343 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 15:18:06 -0700 Subject: [PATCH 24/40] Clean up domain tests more --- readthedocs/rtd_tests/tests/test_domains.py | 26 ++++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/readthedocs/rtd_tests/tests/test_domains.py b/readthedocs/rtd_tests/tests/test_domains.py index 8b8336c1694..3f41645524f 100644 --- a/readthedocs/rtd_tests/tests/test_domains.py +++ b/readthedocs/rtd_tests/tests/test_domains.py @@ -82,47 +82,45 @@ def test_save_parsing(self): class TestCanonical(TestCase): def setUp(self): - self.p = Project( - name='foo', - repo='http://github.com/ericholscher/django-kong', - ) - self.p.save() - self.domain = self.p.domains.create(url='djangokong.com', canonical=True) + self.project = get(Project) + self.domain = self.project.domains.create(canonical=True) def test_canonical_clean(self): # Only a url - self.p.canonical_url = "djangokong.com" - self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") + self.domain.url = "djangokong.com" + self.domain.save() + self.assertEqual(self.project.clean_canonical_url, "http://djangokong.com/") # Extra bits in the URL self.domain.url = "http://djangokong.com/en/latest/" self.domain.save() - self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") + self.assertEqual(self.project.clean_canonical_url, "http://djangokong.com/") self.domain.url = "http://djangokong.com//" self.domain.save() - self.assertEqual(self.p.clean_canonical_url, "http://djangokong.com/") + self.assertEqual(self.project.clean_canonical_url, "http://djangokong.com/") # Subdomain self.domain.url = "foo.djangokong.com" self.domain.save() - self.assertEqual(self.p.clean_canonical_url, "http://foo.djangokong.com/") + self.assertEqual(self.project.clean_canonical_url, "http://foo.djangokong.com/") # Https self.domain.url = "https://djangokong.com//" self.domain.save() - self.assertEqual(self.p.clean_canonical_url, "https://djangokong.com/") + self.assertEqual(self.project.clean_canonical_url, "https://djangokong.com/") self.domain.url = "https://foo.djangokong.com//" self.domain.save() - self.assertEqual(self.p.clean_canonical_url, "https://foo.djangokong.com/") + self.assertEqual(self.project.clean_canonical_url, "https://foo.djangokong.com/") class TestAPI(TestCase): def setUp(self): self.project = get(Project) - self.domain = self.project.domains.create(url='djangokong.com', canonical=True) + self.domain = self.project.domains.create(url='djangokong.com') def test_basic_api(self): resp = self.client.get('/api/v2/domain/') self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) self.assertEqual(obj['results'][0]['url'], 'djangokong.com') + self.assertEqual(obj['results'][0]['canonical'], False) From 51f42da22ce6db51bcebd981c23ee80a2bfd149c Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 15:36:10 -0700 Subject: [PATCH 25/40] Clean up domain forms --- readthedocs/projects/forms.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 57ddb12d0e5..36e4fc0984a 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -114,20 +114,9 @@ class Meta: 'documentation_type', 'language', 'programming_language', 'project_url', - #'canonical_url', 'tags', ) - def __init__(self, *args, **kwargs): - super(ProjectExtraForm, self).__init__(*args, **kwargs) - #self.fields['canonical_url'].widget.attrs['placeholder'] = self.placehold_canonical_url() - - def placehold_canonical_url(self): - return choice([ - 'http://docs.fabfile.org', - 'http://example.readthedocs.org', - ]) - class ProjectAdvancedForm(ProjectTriggerBuildMixin, ProjectForm): python_interpreter = forms.ChoiceField( @@ -183,7 +172,6 @@ class Meta: 'documentation_type', 'language', 'programming_language', 'project_url', - # 'canonical_url', 'tags', ) @@ -445,10 +433,10 @@ def __init__(self, *args, **kwargs): super(DomainForm, self).__init__(*args, **kwargs) def clean_canonical(self): - canonical = self.cleaned_data.get('canonical', False) + canonical = self.cleaned_data['canonical'] if canonical and Domain.objects.filter( project=self.project, canonical=True - ).exclude(project__pk=self.project.pk).exists(): + ).exclude(url=self.cleaned_data['url']).exists(): raise forms.ValidationError(_(u'Only 1 Domain can be canonical at a time.')) return canonical From 70ea09d77d64bcbe2139274e91bb1160eb6862eb Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 19 Aug 2015 16:43:27 -0700 Subject: [PATCH 26/40] Clean up views and make sure they are login required --- readthedocs/projects/views/base.py | 2 +- readthedocs/projects/views/private.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/readthedocs/projects/views/base.py b/readthedocs/projects/views/base.py index 1d385530227..2a0399ef7bc 100644 --- a/readthedocs/projects/views/base.py +++ b/readthedocs/projects/views/base.py @@ -1,7 +1,7 @@ from django.shortcuts import get_object_or_404 from django.core.urlresolvers import reverse -from readthedocs.projects.models import Project, Domain +from readthedocs.projects.models import Project class ProjectOnboardMixin(object): diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 362f9ae3dce..7a7e5876b39 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -680,22 +680,22 @@ def project_version_delete_html(request, project_slug, version_slug): reverse('project_version_list', kwargs={'project_slug': project_slug})) -class DomainList(ProjectAdminMixin, ListViewWithForm): - template_name = 'projects/domain_list.html' +class DomainMixin(ProjectAdminMixin, PrivateViewMixin): model = Domain form_class = DomainForm -class DomainCreate(ProjectAdminMixin, CreateView): - model = Domain - form_class = DomainForm +class DomainList(DomainMixin, ListViewWithForm): + pass -class DomainUpdate(ProjectAdminMixin, UpdateView): - model = Domain - form_class = DomainForm +class DomainCreate(DomainMixin, CreateView): + pass -class DomainDelete(ProjectAdminMixin, DeleteView): - model = Domain +class DomainUpdate(DomainMixin, UpdateView): + pass + +class DomainDelete(DomainMixin, DeleteView): + pass From 3da467b3758c3cc6b30ba413faf047745b3644ba Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 10:15:38 -0700 Subject: [PATCH 27/40] Fix comments and merge fail --- readthedocs/projects/models.py | 6 ++---- readthedocs/projects/views/base.py | 8 ++++---- readthedocs/projects/views/private.py | 4 ---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 2a654cd1415..3b561a4d5ee 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -942,10 +942,7 @@ def __unicode__(self): class Domain(models.Model): project = models.ForeignKey(Project, related_name='domains') - objects = RelatedProjectManager() - url = models.URLField(_('URL'), unique=True) - machine = models.BooleanField( default=False, help_text=_('This URL was auto-created') ) @@ -955,9 +952,10 @@ class Domain(models.Model): canonical = models.BooleanField( default=False, help_text=_('This URL is the primary one where the documentation is served from.') ) - count = models.IntegerField(default=0, help_text=_('Number of times this domain has been hit.')) + objects = RelatedProjectManager() + class Meta: ordering = ('-canonical', '-machine', 'url') diff --git a/readthedocs/projects/views/base.py b/readthedocs/projects/views/base.py index d464c0a79c4..beff422be10 100644 --- a/readthedocs/projects/views/base.py +++ b/readthedocs/projects/views/base.py @@ -39,7 +39,7 @@ class ProjectAdminMixin(object): This mixin uses several class level variables project_url_field - The URL kwarg name for the organization slug + The URL kwarg name for the project slug ''' @@ -50,7 +50,7 @@ def get_queryset(self): return self.model.objects.filter(project=self.project) def get_project(self): - '''Return organization determined by url kwarg''' + '''Return project determined by url kwarg''' if self.project_url_field not in self.kwargs: return None return get_object_or_404( @@ -59,13 +59,13 @@ def get_project(self): ) def get_context_data(self, **kwargs): - '''Add organization to context data''' + '''Add project 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''' + '''Pass in project to form class instance''' kwargs['project'] = self.get_project() return self.form_class(data, files, **kwargs) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 0686c3ea2e8..7e706cbfb54 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -322,11 +322,7 @@ def post(self, request, *args, **kwargs): class ImportDemoView(PrivateViewMixin, View): -<<<<<<< HEAD '''View to pass request on to import form to import demo project''' -======= - """View to pass request on to import form to import demo project""" ->>>>>>> origin/master form_class = ProjectBasicsForm request = None From 9e1a62f0fa121fa011d7a891c57710d83e90b5fd Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 10:25:59 -0700 Subject: [PATCH 28/40] Fix migrations --- ..._cname_modeling.py => 0006_add_domain_models.py} | 13 ++----------- ...nical_data.py => 0007_migrate_canonical_data.py} | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) rename readthedocs/projects/migrations/{0004_add_cname_modeling.py => 0006_add_domain_models.py} (57%) rename readthedocs/projects/migrations/{0005_migrate_canonical_data.py => 0007_migrate_canonical_data.py} (92%) diff --git a/readthedocs/projects/migrations/0004_add_cname_modeling.py b/readthedocs/projects/migrations/0006_add_domain_models.py similarity index 57% rename from readthedocs/projects/migrations/0004_add_cname_modeling.py rename to readthedocs/projects/migrations/0006_add_domain_models.py index 2d530588e51..82e75b4cd0f 100644 --- a/readthedocs/projects/migrations/0004_add_cname_modeling.py +++ b/readthedocs/projects/migrations/0006_add_domain_models.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('projects', '0003_project_cdn_enabled'), + ('projects', '0005_sync_project_model'), ] operations = [ @@ -20,19 +20,10 @@ class Migration(migrations.Migration): ('cname', models.BooleanField(default=False, help_text='This URL is a CNAME for the project')), ('canonical', models.BooleanField(default=False, help_text='This URL is the primary one where the documentation is served from.')), ('count', models.IntegerField(default=0, help_text='Number of times this domain has been hit.')), + ('project', models.ForeignKey(related_name='domains', to='projects.Project')), ], options={ 'ordering': ('-canonical', '-machine', 'url'), }, ), - 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/migrations/0005_migrate_canonical_data.py b/readthedocs/projects/migrations/0007_migrate_canonical_data.py similarity index 92% rename from readthedocs/projects/migrations/0005_migrate_canonical_data.py rename to readthedocs/projects/migrations/0007_migrate_canonical_data.py index af8a9075fdb..9494ac13f07 100644 --- a/readthedocs/projects/migrations/0005_migrate_canonical_data.py +++ b/readthedocs/projects/migrations/0007_migrate_canonical_data.py @@ -18,7 +18,7 @@ def migrate_canonical(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('projects', '0004_add_cname_modeling'), + ('projects', '0006_add_domain_models'), ] operations = [ From cc0c23f7c5c1cfe00130d08a15f28c4406f1d71e Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 10:39:35 -0700 Subject: [PATCH 29/40] Kill copypasta --- readthedocs/restapi/views/model_views.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py index a1f1ec711a5..e5003b4d7d6 100644 --- a/readthedocs/restapi/views/model_views.py +++ b/readthedocs/restapi/views/model_views.py @@ -202,10 +202,6 @@ class NotificationViewSet(viewsets.ReadOnlyModelViewSet): model = EmailHook 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) @@ -216,8 +212,4 @@ class DomainViewSet(viewsets.ReadOnlyModelViewSet): 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 f49ebd2b09118b293de5a01fc66336af17d63819 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 10:48:29 -0700 Subject: [PATCH 30/40] Fix template logic --- readthedocs/templates/projects/domain_form.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/readthedocs/templates/projects/domain_form.html b/readthedocs/templates/projects/domain_form.html index 956ac1eea57..9861f9f4f95 100644 --- a/readthedocs/templates/projects/domain_form.html +++ b/readthedocs/templates/projects/domain_form.html @@ -28,10 +28,11 @@

          {% trans "Existing Domains" %}

          {% trans "Choose which project you would like to add as a domain." %}

          {% if domain %} - {% csrf_token %} + {% url 'projects_domains_create' project.slug as action_url %} {% else %} - {% csrf_token %} + {% url 'projects_domains_edit' project.slug domain.pk as action_url %} {% endif %} + {% csrf_token %} {{ form.as_p }}

          From b93cd05ae337049b2dac01d9f50b7aacea006ad3 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 10:51:10 -0700 Subject: [PATCH 31/40] Fix import and linting --- readthedocs/projects/models.py | 3 ++- readthedocs/projects/symlinks.py | 12 ++++++++++-- readthedocs/restapi/views/model_views.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 3b561a4d5ee..cfce70b943f 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -950,7 +950,8 @@ class Domain(models.Model): default=False, help_text=_('This URL is a CNAME for the project') ) canonical = models.BooleanField( - default=False, help_text=_('This URL is the primary one where the documentation is served from.') + default=False, + help_text=_('This URL is the primary one where the documentation is served from.') ) count = models.IntegerField(default=0, help_text=_('Number of times this domain has been hit.')) diff --git a/readthedocs/projects/symlinks.py b/readthedocs/projects/symlinks.py index f89584e3015..58d7c9a513d 100644 --- a/readthedocs/projects/symlinks.py +++ b/readthedocs/projects/symlinks.py @@ -25,7 +25,11 @@ def symlink_cnames(version): """ domains = Domain.objects.filter(project=version.project, cname=True) for domain in domains: - log.debug(LOG_TEMPLATE.format(project=version.project.slug, version=version.slug, msg="Symlinking CNAME: %s" % domain.clean_host)) + log.debug(LOG_TEMPLATE.format( + project=version.project.slug, + version=version.slug, + msg="Symlinking CNAME: %s" % domain.clean_host) + ) docs_dir = version.project.rtd_build_path(version.slug) # Chop off the version from the end. docs_dir = '/'.join(docs_dir.split('/')[:-1]) @@ -35,7 +39,11 @@ def symlink_cnames(version): run_on_app_servers('ln -nsf %s %s' % (docs_dir, symlink)) # New symlink location new_docs_dir = version.project.doc_path - new_cname_symlink = os.path.join(getattr(settings, 'SITE_ROOT'), 'cnametoproject', domain.clean_host) + new_cname_symlink = os.path.join( + getattr(settings, 'SITE_ROOT'), + 'cnametoproject', + domain.clean_host, + ) run_on_app_servers('mkdir -p %s' % '/'.join(new_cname_symlink.split('/')[:-1])) run_on_app_servers('ln -nsf %s %s' % (new_docs_dir, new_cname_symlink)) diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py index e5003b4d7d6..bed17c5c592 100644 --- a/readthedocs/restapi/views/model_views.py +++ b/readthedocs/restapi/views/model_views.py @@ -10,6 +10,7 @@ from readthedocs.builds.constants import TAG from readthedocs.builds.filters import VersionFilter from readthedocs.builds.models import Build, BuildCommandResult, Version +from readthedocs.restapi import utils as api_utils from readthedocs.core.utils import trigger_build from readthedocs.oauth import utils as oauth_utils from readthedocs.projects.filters import ProjectFilter From 8ca4056da4bce471fa87e6e8a10f305c55f2fa53 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 11:10:14 -0700 Subject: [PATCH 32/40] Post to random channel to reduce noise --- .travis.yml | 2 +- readthedocs/rtd_tests/tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7d6d4a06b4..84c0abfeabd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,6 @@ after_success: notifications: slack: rooms: - - readthedocs:y3hjODOi7EIz1JAbD1Zb41sz#general + - readthedocs:y3hjODOi7EIz1JAbD1Zb41sz#random on_success: change on_failure: always diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py index bb8575fbd1c..638b1434f1c 100644 --- a/readthedocs/rtd_tests/tests/test_api.py +++ b/readthedocs/rtd_tests/tests/test_api.py @@ -67,7 +67,7 @@ def _try_post(): _try_post() api_user = get(User, staff=False, password='test') - assert api_user.is_staff == False + assert api_user.is_staff is False client.force_authenticate(user=api_user) _try_post() From c23f3a14b7de1db4bde56603628cb34bdac8d158 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 11:12:00 -0700 Subject: [PATCH 33/40] Fix build serializer --- readthedocs/restapi/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index e1bc1af72fe..c1be53c820a 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -52,14 +52,14 @@ class BuildSerializer(serializers.ModelSerializer): """Readonly version of the build serializer, used for user facing display""" + commands = BuildCommandSerializer(many=True, read_only=True) + state_display = serializers.ReadOnlyField(source='get_state_display') + class VersionFullSerializer(VersionSerializer): '''Serializer for all fields on version model''' - commands = BuildCommandSerializer(many=True, read_only=True) - state_display = serializers.ReadOnlyField(source='get_state_display') - class Meta: model = Build exclude = ('builder',) From 155823b993d3d5d0d084ef1c3d2719ea432dc299 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Tue, 15 Sep 2015 11:13:15 -0700 Subject: [PATCH 34/40] Delete merge flail --- readthedocs/restapi/serializers.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index c1be53c820a..e4b53126533 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -18,14 +18,6 @@ class Meta: ) -class ProjectFullSerializer(ProjectSerializer): - - '''Serializer for all fields on project model''' - - class Meta: - model = Project - - class VersionSerializer(serializers.ModelSerializer): project = ProjectSerializer() downloads = serializers.DictField(source='get_downloads', read_only=True) From b15d0c211b8e2432cd40acd729519b15773df101 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 16 Sep 2015 17:28:43 -0700 Subject: [PATCH 35/40] Clean up copypasta fail --- readthedocs/restapi/serializers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index e4b53126533..b4f3c2b5793 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -47,11 +47,6 @@ class BuildSerializer(serializers.ModelSerializer): commands = BuildCommandSerializer(many=True, read_only=True) state_display = serializers.ReadOnlyField(source='get_state_display') - -class VersionFullSerializer(VersionSerializer): - - '''Serializer for all fields on version model''' - class Meta: model = Build exclude = ('builder',) From 652852713245d2d6ba99f8bb09e14552cd12a2e2 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 16 Sep 2015 17:30:05 -0700 Subject: [PATCH 36/40] Fix quotes --- readthedocs/core/mixins.py | 8 ++++---- readthedocs/projects/forms.py | 8 ++++---- readthedocs/projects/views/base.py | 12 ++++++------ readthedocs/projects/views/private.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/readthedocs/core/mixins.py b/readthedocs/core/mixins.py index e19398877c1..9c803dad756 100644 --- a/readthedocs/core/mixins.py +++ b/readthedocs/core/mixins.py @@ -1,6 +1,6 @@ -''' +""" Common mixin classes for views -''' +""" from django.conf import settings @@ -9,7 +9,7 @@ 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): context = super(StripeMixin, self).get_context_data(**kwargs) @@ -21,7 +21,7 @@ def get_context_data(self, **kwargs): class ListViewWithForm(ListView): - '''List view that also exposes a create form''' + """List view that also exposes a create form""" def get_context_data(self, **kwargs): context = super(ListViewWithForm, self).get_context_data(**kwargs) diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 1651dadc9e3..6fbe1c4bae6 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -43,11 +43,11 @@ def save(self, commit=True): class ProjectTriggerBuildMixin(object): - '''Mixin to trigger build on form save + """Mixin to trigger build on form save This should be replaced with signals instead of calling trigger_build explicitly. - ''' + """ def save(self, commit=True): """Trigger build on commit save""" @@ -59,14 +59,14 @@ def save(self, commit=True): class ProjectBackendForm(forms.Form): - '''Get the import backend''' + """Get the import backend""" backend = forms.CharField() class ProjectBasicsForm(ProjectForm): - '''Form for basic project fields''' + """Form for basic project fields""" class Meta: model = Project diff --git a/readthedocs/projects/views/base.py b/readthedocs/projects/views/base.py index beff422be10..6e67cb46fe9 100644 --- a/readthedocs/projects/views/base.py +++ b/readthedocs/projects/views/base.py @@ -6,7 +6,7 @@ class ProjectOnboardMixin(object): - '''Add project onboard context data to project object views''' + """Add project onboard context data to project object views""" def get_context_data(self, **kwargs): """Add onboard context data""" @@ -34,14 +34,14 @@ def get_context_data(self, **kwargs): # Mixins class ProjectAdminMixin(object): - '''Mixin class that provides project sublevel objects + """Mixin class that provides project sublevel objects This mixin uses several class level variables project_url_field The URL kwarg name for the project slug - ''' + """ project_url_field = 'project' @@ -50,7 +50,7 @@ def get_queryset(self): return self.model.objects.filter(project=self.project) def get_project(self): - '''Return project determined by url kwarg''' + """Return project determined by url kwarg""" if self.project_url_field not in self.kwargs: return None return get_object_or_404( @@ -59,13 +59,13 @@ def get_project(self): ) def get_context_data(self, **kwargs): - '''Add project to context data''' + """Add project 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 project to form class instance''' + """Pass in project to form class instance""" kwargs['project'] = self.get_project() return self.form_class(data, files, **kwargs) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 7e706cbfb54..a37c39bf464 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -322,7 +322,7 @@ def post(self, request, *args, **kwargs): 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 request = None From a548d60d0f412249803b94ec46524e2690907636 Mon Sep 17 00:00:00 2001 From: Eric Holscher Date: Wed, 16 Sep 2015 18:03:49 -0700 Subject: [PATCH 37/40] Kill unused JS in templates --- readthedocs/templates/projects/domain_form.html | 12 ------------ readthedocs/templates/projects/domain_list.html | 12 ------------ 2 files changed, 24 deletions(-) diff --git a/readthedocs/templates/projects/domain_form.html b/readthedocs/templates/projects/domain_form.html index 9861f9f4f95..3012e7c5853 100644 --- a/readthedocs/templates/projects/domain_form.html +++ b/readthedocs/templates/projects/domain_form.html @@ -40,15 +40,3 @@

          {% trans "Existing Domains" %}

          {% 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 index cfcd7a92563..eb012bb2aa5 100644 --- a/readthedocs/templates/projects/domain_list.html +++ b/readthedocs/templates/projects/domain_list.html @@ -39,15 +39,3 @@

          {% 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 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 }}