Skip to content

Commit a7b4595

Browse files
committed
Add feature flipping models to Projects, plus example feature flip
Also, use a new pattern for API instance objects Because we're returning a list of feature keys, which is a many2many field, we can't use the Project class as we were using it. For instance, this is impossible in Django, because the m2m field requires saving first: proj = Project(features=[Feature(feature='foo')], ...) # or proj = Project(...) proj.features.add(Feature(feature='foo')) This might make the modeling around API model objects more grokable
1 parent fbe860f commit a7b4595

File tree

7 files changed

+150
-8
lines changed

7 files changed

+150
-8
lines changed

readthedocs/doc_builder/python_environments.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from readthedocs.doc_builder.config import ConfigWrapper
1212
from readthedocs.doc_builder.loader import get_builder_class
1313
from readthedocs.projects.constants import LOG_TEMPLATE
14+
from readthedocs.projects.models import Feature
1415

1516
log = logging.getLogger(__name__)
1617

@@ -128,8 +129,14 @@ def install_core_requirements(self):
128129
if self.project.documentation_type == 'mkdocs':
129130
requirements.append('mkdocs==0.15.0')
130131
else:
131-
requirements.extend(['sphinx==1.5.3', 'sphinx-rtd-theme<0.3',
132-
'readthedocs-sphinx-ext<0.6'])
132+
if self.project.has_feature(Feature.USE_SPHINX_LATEST):
133+
requirements.append('sphinx<1.7')
134+
else:
135+
requirements.append('sphinx==1.5.6')
136+
requirements.extend([
137+
'sphinx-rtd-theme<0.3',
138+
'readthedocs-sphinx-ext<0.6'
139+
])
133140

134141
cmd = [
135142
'python',

readthedocs/projects/admin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from readthedocs.notifications.views import SendNotificationView
1515

1616
from .notifications import ResourceUsageNotification
17-
from .models import (Project, ImportedFile,
17+
from .models import (Project, ImportedFile, Feature,
1818
ProjectRelationship, EmailHook, WebHook, Domain)
1919
from .tasks import remove_dir
2020

@@ -180,8 +180,14 @@ class DomainAdmin(admin.ModelAdmin):
180180
model = Domain
181181

182182

183+
class FeatureAdmin(admin.ModelAdmin):
184+
model = Feature
185+
filter_horizontal = ('projects',)
186+
187+
183188
admin.site.register(Project, ProjectAdmin)
184189
admin.site.register(ImportedFile, ImportedFileAdmin)
185190
admin.site.register(Domain, DomainAdmin)
191+
admin.site.register(Feature, FeatureAdmin)
186192
admin.site.register(EmailHook)
187193
admin.site.register(WebHook)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.12 on 2017-10-05 01:21
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('projects', '0018_fix-translation-model'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='Feature',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('feature', models.CharField(max_length=32, unique=True, verbose_name='Project feature tag')),
20+
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
21+
],
22+
),
23+
migrations.AddField(
24+
model_name='feature',
25+
name='projects',
26+
field=models.ManyToManyField(blank=True, related_name='features', to='projects.Project'),
27+
),
28+
]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.12 on 2017-10-05 01:06
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations
6+
7+
8+
FEATURE = 'use_sphinx_latest'
9+
DESCRIPTION = 'Use latest Sphinx version'
10+
11+
12+
def add_feature(apps, schema_editor):
13+
Feature = apps.get_model('projects', 'Feature')
14+
(_, created) = Feature.objects.get_or_create(
15+
feature=FEATURE,
16+
description=DESCRIPTION
17+
)
18+
19+
20+
def remove_feature(apps, schema_editor):
21+
Feature = apps.get_model('projects', 'Feature')
22+
Feature.objects.filter(feature=FEATURE).delete()
23+
24+
25+
class Migration(migrations.Migration):
26+
27+
dependencies = [
28+
('projects', '0019_add-feature-flags'),
29+
]
30+
31+
operations = [
32+
migrations.RunPython(add_feature, remove_feature)
33+
]

readthedocs/projects/models.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,35 @@ def add_comment(self, version_slug, page, content_hash, commit, user, text):
821821
hash=content_hash, commit=commit)
822822
return node.comments.create(user=user, text=text)
823823

824+
def has_feature(self, feature):
825+
"""Does project have existing feature flag"""
826+
return self.features.filter(feature=feature).exists()
827+
828+
829+
class APIProject(Project):
830+
831+
"""Project object from API data deserialization
832+
833+
This is to preserve the Project model methods for use in builder instances.
834+
Properties can be read only here, as the builders don't need write access to
835+
a number of attributes.
836+
"""
837+
838+
features = []
839+
840+
class Meta:
841+
proxy = True
842+
843+
def __init__(self, *args, **kwargs):
844+
self.features = kwargs.pop('features', [])
845+
super(APIProject, self).__init__(*args, **kwargs)
846+
847+
def save(self, *args, **kwargs):
848+
return 0
849+
850+
def has_feature(self, feature):
851+
return feature in self.features
852+
824853

825854
@python_2_unicode_compatible
826855
class ImportedFile(models.Model):
@@ -921,3 +950,30 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
921950
from readthedocs.projects import tasks
922951
broadcast(type='app', task=tasks.symlink_domain, args=[self.project.pk, self.pk, True])
923952
super(Domain, self).delete(*args, **kwargs)
953+
954+
955+
@python_2_unicode_compatible
956+
class Feature(models.Model):
957+
958+
"""Project feature flags"""
959+
960+
# Feature constants - this is not a exhaustive list of features, features
961+
# are arbitrary and maintained via data migrations.
962+
963+
#: Use latest Sphinx
964+
USE_SPHINX_LATEST = 'use_sphinx_latest'
965+
966+
projects = models.ManyToManyField(
967+
Project,
968+
related_name='features',
969+
blank=True,
970+
)
971+
feature = models.CharField(
972+
_('Project feature tag'),
973+
max_length=32,
974+
unique=True,
975+
)
976+
description = models.TextField(_('Description'), blank=True, null=True)
977+
978+
def __str__(self):
979+
return self.description or self.feature

readthedocs/projects/utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,10 @@ def make_api_version(version_data):
192192

193193
def make_api_project(project_data):
194194
"""Make mock Project instance from API return"""
195-
from readthedocs.projects.models import Project
195+
from readthedocs.projects.models import APIProject
196196
for key in ['users', 'resource_uri', 'absolute_url', 'downloads',
197197
'main_language_project', 'related_projects']:
198198
if key in project_data:
199199
del project_data[key]
200-
project = Project(**project_data)
201-
project.save = _new_save
200+
project = APIProject(**project_data)
202201
return project

readthedocs/restapi/serializers.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Defines serializers for each of our models."""
22

33
from __future__ import absolute_import
4-
from builtins import object
54

5+
from builtins import object
66
from rest_framework import serializers
77

88
from readthedocs.builds.models import Build, BuildCommandResult, Version
9-
from readthedocs.projects.models import Project, Domain
109
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
10+
from readthedocs.projects.models import Project, Domain
1111

1212

1313
class ProjectSerializer(serializers.ModelSerializer):
@@ -28,6 +28,18 @@ class Meta(object):
2828

2929
class ProjectAdminSerializer(ProjectSerializer):
3030

31+
"""Project serializer for admin only access
32+
33+
Includes special internal fields that don't need to be exposed through the
34+
general API, mostly for fields used in the build process
35+
"""
36+
37+
features = serializers.SlugRelatedField(
38+
many=True,
39+
read_only=True,
40+
slug_field='feature',
41+
)
42+
3143
class Meta(ProjectSerializer.Meta):
3244
fields = ProjectSerializer.Meta.fields + (
3345
'enable_epub_build',
@@ -44,6 +56,7 @@ class Meta(ProjectSerializer.Meta):
4456
'skip',
4557
'requirements_file',
4658
'python_interpreter',
59+
'features',
4760
)
4861

4962

0 commit comments

Comments
 (0)