Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Domain model to track project Domains #1575

Merged
merged 42 commits into from
Sep 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ef05c6a
Add Domain model
ericholscher Aug 19, 2015
eba84ea
Add Admin views for Domains
ericholscher Aug 19, 2015
86d416c
Add API endpoint for domains
ericholscher Aug 19, 2015
27fb6b3
Add domain templates
ericholscher Aug 19, 2015
09ecf2d
Add Domain tracking to middleware & test it
ericholscher Aug 19, 2015
dc59895
Fix linting errors
ericholscher Aug 19, 2015
8aa257f
Clean up domain admin a bit
ericholscher Aug 19, 2015
3669f48
Clean up tests
ericholscher Aug 19, 2015
d1e7a6f
Add nicer UX and check for only 1 active canonical model
ericholscher Aug 19, 2015
6b6eaa2
Add data migration
ericholscher Aug 19, 2015
0c66e47
Add Domain inline for Project admin
ericholscher Aug 19, 2015
bcfe13a
Remove canonical_url usage in Project forms
ericholscher Aug 19, 2015
318457d
Use Domain objects for CNAME symlinking
ericholscher Aug 19, 2015
cec9706
Add tests for symlinking code
ericholscher Aug 19, 2015
a594e63
Make domains nicely cleaned before save
ericholscher Aug 19, 2015
9991b77
Add domain model tests
ericholscher Aug 19, 2015
da2c644
Clean up a bit of the modeling & use basic count
ericholscher Aug 19, 2015
a9e512f
Fix tests & logic
ericholscher Aug 19, 2015
78e8497
Use Domain modeling for canonical setup
ericholscher Aug 19, 2015
0d9a5c9
Clean up symlinking and symlink testing
ericholscher Aug 19, 2015
add1507
Clean up canonical tests for new scheme
ericholscher Aug 19, 2015
5620889
Remove active concept, and just have folks delete ones they don’t want.
ericholscher Aug 19, 2015
cb7d4c6
Add basic test case for API, to verify it doesn’t break
ericholscher Aug 19, 2015
116f18b
Clean up domain tests more
ericholscher Aug 19, 2015
51f42da
Clean up domain forms
ericholscher Aug 19, 2015
70ea09d
Clean up views and make sure they are login required
ericholscher Aug 19, 2015
80bf3dd
Merge remote-tracking branch 'origin/master' into add-domain-model
ericholscher Aug 19, 2015
e3001b3
Merge remote-tracking branch 'origin/master' into add-domain-model
ericholscher Sep 15, 2015
3da467b
Fix comments and merge fail
ericholscher Sep 15, 2015
9e1a62f
Fix migrations
ericholscher Sep 15, 2015
cc0c23f
Kill copypasta
ericholscher Sep 15, 2015
f49ebd2
Fix template logic
ericholscher Sep 15, 2015
b93cd05
Fix import and linting
ericholscher Sep 15, 2015
8ca4056
Post to random channel to reduce noise
ericholscher Sep 15, 2015
c23f3a1
Fix build serializer
ericholscher Sep 15, 2015
155823b
Delete merge flail
ericholscher Sep 15, 2015
b15d0c2
Clean up copypasta fail
ericholscher Sep 17, 2015
6528527
Fix quotes
ericholscher Sep 17, 2015
a548d60
Kill unused JS in templates
ericholscher Sep 17, 2015
227d5a3
Clean up project injection on the Domain form
ericholscher Sep 17, 2015
0075362
Fix logic around Create/Delete of domains
ericholscher Sep 17, 2015
6cb9d9d
CLean up more templates
ericholscher Sep 17, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ after_success:
notifications:
slack:
rooms:
- readthedocs:y3hjODOi7EIz1JAbD1Zb41sz#general
- readthedocs:y3hjODOi7EIz1JAbD1Zb41sz#random
on_success: change
on_failure: always
18 changes: 17 additions & 1 deletion readthedocs/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -71,6 +71,22 @@ 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, created = Domain.objects.get_or_create(
project=proj,
url=host,
)
if created:
domain.machine = True
domain.cname = True
# Track basic domain counts so we know which are heavily used
domain.count = domain.count + 1
domain.save()
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))
Expand Down
19 changes: 16 additions & 3 deletions readthedocs/core/mixins.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
'''
"""
Common mixin classes for views
'''
"""

from django.conf import settings

from vanilla import ListView


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)
context.update({
'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
16 changes: 12 additions & 4 deletions readthedocs/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,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


Expand All @@ -32,6 +31,10 @@ class RedirectInline(admin.TabularInline):
model = Redirect


class DomainInline(admin.TabularInline):
model = Domain


class ProjectAdmin(GuardedModelAdmin):

"""Project model admin view"""
Expand All @@ -42,7 +45,7 @@ class ProjectAdmin(GuardedModelAdmin):
'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')


Expand All @@ -53,7 +56,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)
37 changes: 24 additions & 13 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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


Expand Down Expand Up @@ -126,20 +126,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):

Expand Down Expand Up @@ -198,7 +187,6 @@ class Meta:
'documentation_type',
'language', 'programming_language',
'project_url',
'canonical_url',
'tags',
)

Expand Down Expand Up @@ -476,3 +464,26 @@ def save(self, **_):
to_url=self.cleaned_data['to_url'],
)
return redirect


class DomainForm(forms.ModelForm):
project = forms.CharField(widget=forms.HiddenInput(), required=False)

class Meta:
model = Domain
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(
project=self.project, canonical=True
).exclude(url=self.cleaned_data['url']).exists():
raise forms.ValidationError(_(u'Only 1 Domain can be canonical at a time.'))
return canonical
29 changes: 29 additions & 0 deletions readthedocs/projects/migrations/0006_add_domain_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations


class Migration(migrations.Migration):

dependencies = [
('projects', '0005_sync_project_model'),
]

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='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.')),
('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'),
},
),
]
26 changes: 26 additions & 0 deletions readthedocs/projects/migrations/0007_migrate_canonical_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- 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,
)
print "Added {url} to {project}".format(url=domain.url, project=project.name)


class Migration(migrations.Migration):

dependencies = [
('projects', '0006_add_domain_models'),
]

operations = [
migrations.RunPython(migrate_canonical)
]
45 changes: 41 additions & 4 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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
Expand Down Expand Up @@ -437,22 +438,26 @@ def canonical_domain(self):

@property
def clean_canonical_url(self):
"""Normalize canonical URL field"""
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
Expand Down Expand Up @@ -933,3 +938,35 @@ class WebHook(Notification):

def __unicode__(self):
return self.url


class Domain(models.Model):
project = models.ForeignKey(Project, related_name='domains')
url = models.URLField(_('URL'), unique=True)
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.')
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good practice to prefix boolean attributes/field names with is_, e.g. is_canonical which makes it clear what to expect from field and is better for reading expressions like domain.is_cname.

We don't follow that practice in the rest of the project, but maybe here is a good place to start with it?

count = models.IntegerField(default=0, help_text=_('Number of times this domain has been hit.'))

objects = RelatedProjectManager()

class Meta:
ordering = ('-canonical', '-machine', 'url')

def __unicode__(self):
return "{url} pointed at {project}".format(url=self.url, project=self.project.name)

@property
def clean_host(self):
parsed = urlparse(self.url)
if parsed.scheme or parsed.netloc:
return parsed.netloc
else:
return parsed.path
30 changes: 14 additions & 16 deletions readthedocs/projects/symlinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
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 readthedocs.projects.models import Domain
from readthedocs.restapi.client import api

log = logging.getLogger(__name__)
Expand All @@ -23,29 +23,27 @@ def symlink_cnames(version):
Link from HOME/user_builds/cnametoproject/<cname> ->
HOME/user_builds/<project>/
"""
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, 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)
)
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.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', cname)
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))

Expand Down
Loading