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

Save config on build model #4749

Merged
merged 32 commits into from Nov 7, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6891db8
New field
stsewd Oct 11, 2018
b1b3620
Change API
stsewd Oct 11, 2018
27d8251
Implement as_dict to config
stsewd Oct 11, 2018
ab5ea04
One more test for the api
stsewd Oct 12, 2018
5f4fb8c
Save config on build
stsewd Oct 12, 2018
9d70812
Simple query
stsewd Oct 12, 2018
13abeda
Test for version.config property
stsewd Oct 12, 2018
98d26a4
Use callable as default
stsewd Oct 15, 2018
660c03a
Update migration
stsewd Oct 23, 2018
d29b6d9
Save on model only if there is a difference
stsewd Oct 23, 2018
7fc1e2b
Merge branch 'master' into save-config-on-build
stsewd Oct 23, 2018
8f62f03
More tests
stsewd Oct 23, 2018
76929ac
Linter
stsewd Oct 23, 2018
7ade872
One more test
stsewd Oct 23, 2018
6f8becc
Better approach for save config
stsewd Oct 23, 2018
07efa1b
Tests
stsewd Oct 23, 2018
715e5ed
Fix test for API
stsewd Oct 23, 2018
dab5860
Update docstrings
stsewd Oct 23, 2018
5aacd63
Merge branch 'master' into save-config-on-build
stsewd Oct 23, 2018
822b793
Check for previous only on first creation or if the object hast changed
stsewd Nov 2, 2018
8c60760
Merge branch 'master' into save-config-on-build
stsewd Nov 2, 2018
7ed110d
Rebase migration
stsewd Nov 2, 2018
2ffdbf6
Use just patch
stsewd Nov 2, 2018
5cce5d8
Docstring
stsewd Nov 5, 2018
9d8266f
Don't reference to empty builds
stsewd Nov 5, 2018
28cd935
Fix varname
stsewd Nov 5, 2018
433b709
Add test
stsewd Nov 5, 2018
b88837d
Docstring
stsewd Nov 5, 2018
4723705
Merge branch 'master' into save-config-on-build
stsewd Nov 5, 2018
043f4e6
Don't expose `_config` on the API
stsewd Nov 6, 2018
79fbb8e
Refactor as feedback
stsewd Nov 7, 2018
f33d54a
Merge branch 'master' into save-config-on-build
stsewd Nov 7, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions readthedocs/builds/migrations/0005_add_config_field.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2018-10-11 14:00
# Generated by Django 1.9.13 on 2018-10-22 19:37
from __future__ import unicode_literals

from django.db import migrations, models
Expand All @@ -17,7 +17,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='build',
name='config',
field=jsonfield.fields.JSONField(default={}, verbose_name='Configuration used in the build'),
field=jsonfield.fields.JSONField(default=dict, verbose_name='Configuration used in the build'),
),
migrations.AlterField(
model_name='build',
Expand Down
46 changes: 44 additions & 2 deletions readthedocs/builds/models.py
Expand Up @@ -17,6 +17,7 @@
from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
Expand Down Expand Up @@ -137,7 +138,7 @@ def config(self):
.order_by('-date')
.first()
)
return last_build.config
return last_build.get_config()

@property
def commit_name(self):
Expand Down Expand Up @@ -478,7 +479,7 @@ class Build(models.Model):
exit_code = models.IntegerField(_('Exit code'), null=True, blank=True)
commit = models.CharField(
_('Commit'), max_length=255, null=True, blank=True)
config = JSONField(_('Configuration used in the build'), default={})
config = JSONField(_('Configuration used in the build'), default=dict)

length = models.IntegerField(_('Build Length'), null=True, blank=True)

Expand All @@ -497,6 +498,47 @@ class Meta(object):
get_latest_by = 'date'
index_together = [['version', 'state', 'type']]

@property
def previous(self):
"""Returns the previous build to this one."""
Copy link
Member

Choose a reason for hiding this comment

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

I'm trying to be more explicit here,

Return the previous build to the current Build (matching the project and version)

date = self.date or timezone.now()
if self.project is not None and self.version is not None:
return (
Build.objects
.filter(
project=self.project,
version=self.version,
date__lt=date,
)
.order_by('-date')
.first()
)
return None

def get_config(self):
"""Get the config from correct object."""
Copy link
Member

Choose a reason for hiding this comment

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

If we are not going to access the config from this object directly, I suggest to use _config for the model field, and make this method a @property called just config. This way, we will avoid potential future problems and mistakes.

The docstring could be more explicit.

Get the config used for this build.

Since we are saving the config into the JSON field only when it differs from the previous one,
this helper returns the correct JSON used for this particular Build object
(it could be stored in this object or one of the previous ones)

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the idea of using _config, also I'd add a @property.set, to set _config through just config. The weird part, would be on the API, but I think we can handle that 🤔

if '__config' in self.config:
return Build.objects.get(pk=self.config['__config']).config
return self.config

def save(self, *args, **kwargs): # noqa
"""
Save object.

To save space on the db we only save the config if it's different
from the previous one.

If the config is the same, we save the pk of the object
that has the **real** config under the ``__config`` key.
Copy link
Member

Choose a reason for hiding this comment

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

The __config field could be more explicit also, like __config_used__build_pk.

Copy link
Member

Choose a reason for hiding this comment

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

Also, it would be good if this is a CONSTANT defined in the class, instead something that it's written many times.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'm not convinced with the name either, not sure about __config_used__build_pk. I don't have better ideas for the name though :/

"""
previous = self.previous
if previous is not None and self.config == previous.get_config():
Copy link
Member

Choose a reason for hiding this comment

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

Do you know how deep the == operator works with dicts?

Also, I think this comparison needs to be a method because it will be more explicit. Like, has_config_changed or similar.

Copy link
Member Author

Choose a reason for hiding this comment

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

previous_pk = previous.pk
if '__config' in previous.config:
previous_pk = previous.config['__config']
self.config = {'__config': previous_pk}
super(Build, self).save(*args, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little worried that we're running this on each Build save. We do this a number of times during a documentation build, and this will be adding a decent number of queries (eg. self.previous is a query, as is previous.config).

Is there a way to set the _config just once when we have changed it?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think there is an is_changed method on django, but I think we can do this using an internal variable.

Copy link
Member

Choose a reason for hiding this comment

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

Yea, I'm just trying to think if there is a way to run this logic just when we are trying to set a config. Perhaps it belongs somewhere else?


def __str__(self):
return ugettext(
'Build {project} for {usernames} ({pk})'.format(
Expand Down
39 changes: 36 additions & 3 deletions readthedocs/rtd_tests/tests/test_api.py
Expand Up @@ -58,9 +58,8 @@ def test_make_build(self):
build = resp.data
self.assertEqual(build['output'], 'Test Output')
self.assertEqual(build['state_display'], 'Cloning')

def test_save_config(self):
"""Test that a superuser can use the API."""
client = APIClient()
client.login(username='super', password='test')
resp = client.post(
Expand All @@ -73,10 +72,44 @@ def test_save_config(self):
format='json',
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
build_one = resp.data
self.assertEqual(build_one['config'], {'one': 'two'})

resp = client.get('/api/v2/build/%s/' % build_one['id'])
self.assertEqual(resp.status_code, 200)
build = resp.data
self.assertEqual(build['config'], {'one': 'two'})

resp = client.get('/api/v2/build/%s/' % build['id'])
def test_save_same_config(self):
client = APIClient()
client.login(username='super', password='test')
resp = client.post(
'/api/v2/build/',
{
'project': 1,
'version': 1,
'config': {'one': 'two'},
},
format='json',
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
build_one = resp.data
self.assertEqual(build_one['config'], {'one': 'two'})

resp = client.post(
'/api/v2/build/',
{
'project': 1,
'version': 1,
'config': {'one': 'two'},
},
format='json',
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
build_two = resp.data
self.assertEqual(build_two['config'], {'__config': build_one['id']})

resp = client.get('/api/v2/build/%s/' % build_one['id'])
self.assertEqual(resp.status_code, 200)
build = resp.data
self.assertEqual(build['config'], {'one': 'two'})
Expand Down
165 changes: 158 additions & 7 deletions readthedocs/rtd_tests/tests/test_builds.py
@@ -1,17 +1,19 @@
from __future__ import absolute_import
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)

import mock
import six

from django.test import TestCase
from django_dynamic_fixture import get
from django_dynamic_fixture import fixture
from django_dynamic_fixture import fixture, get

from readthedocs.projects.models import Project
from readthedocs.builds.models import Version, Build
from readthedocs.builds.models import Build, Version
from readthedocs.doc_builder.config import load_yaml_config
from readthedocs.doc_builder.environments import LocalBuildEnvironment
from readthedocs.doc_builder.python_environments import Virtualenv
from readthedocs.projects.models import Project
from readthedocs.projects.tasks import UpdateDocsTaskStep
from readthedocs.rtd_tests.tests.test_config_integration import create_load

Expand Down Expand Up @@ -247,3 +249,152 @@ def test_save_config_in_build_model(self, load_config, api_v2):
assert build_config['version'] == '1'
assert 'sphinx' in build_config
assert build_config['doctype'] == 'sphinx'


class BuildModelTests(TestCase):

def setUp(self):
self.project = get(Project)
self.version = get(Version, project=self.project)

def test_get_previous_build(self):
build_one = get(
Build,
project=self.project,
version=self.version,
config={'version': 1}
)
build_two = get(
Build,
project=self.project,
version=self.version,
config={'version': 2}
)
build_three = get(
Build,
project=self.project,
version=self.version,
config={'version': 3},
success=False,
)

self.assertIsNone(build_one.previous)
self.assertEqual(build_two.previous, build_one)
self.assertEqual(build_three.previous, build_two)
self.assertEqual(build_three.previous.previous, build_one)

def test_normal_save_config(self):
build = get(
Build,
project=self.project,
version=self.version,
config={'version': 1}
)
self.assertEqual(build.config, {'version': 1})

build.config = {'version': 2}
build.save()
self.assertEqual(build.config, {'version': 2})

def test_save_same_config(self):
stsewd marked this conversation as resolved.
Show resolved Hide resolved
build_one = get(
Build,
project=self.project,
version=self.version,
config={}
)

build_two = get(
Build,
project=self.project,
version=self.version,
config={'version': 2}
)
self.assertEqual(build_two.config, {'version': 2})

def test_save_same_config_previous_empty(self):
build_one = get(
Build,
project=self.project,
version=self.version,
config={}
)

build_two = get(
Build,
project=self.project,
version=self.version,
config={}
)
self.assertEqual(build_two.config, {})
build_two.config = {'version': 2}
build_two.save()
self.assertEqual(build_two.config, {'version': 2})

def test_do_not_save_same_config(self):
build_one = get(
Build,
project=self.project,
version=self.version,
config={'version': 1},
)

build_two = get(
Build,
project=self.project,
version=self.version,
config={},
)
build_two.config = {'version': 1}
build_two.save()
self.assertEqual(build_two.config, {'__config': build_one.pk})
self.assertEqual(build_two.get_config(), {'version': 1})

def test_do_not_save_same_config_nested(self):
build_one = get(
Build,
project=self.project,
version=self.version,
config={},
)
build_one.config = {'version': 1}
build_one.save()

build_two = get(
Build,
project=self.project,
version=self.version,
config={},
)
build_two.config = {'version': 1}
build_two.save()

build_three = get(
Build,
project=self.project,
version=self.version,
config={},
)
build_three.config = {'version': 1}
build_three.save()

build_four = get(
Build,
project=self.project,
version=self.version,
config={},
)
build_four.config = {'version': 2}
build_four.save()

self.assertEqual(build_one.get_config(), {'version': 1})
self.assertEqual(build_one.config, {'version': 1})

self.assertEqual(build_two.config, {'__config': build_one.pk})
self.assertEqual(build_three.config, {'__config': build_one.pk})

self.assertEqual(build_two.get_config(), {'version': 1})
self.assertEqual(build_three.get_config(), {'version': 1})

self.assertEqual(build_four.get_config(), {'version': 2})
self.assertEqual(build_four.config, {'version': 2})
42 changes: 42 additions & 0 deletions readthedocs/rtd_tests/tests/test_version_config.py
Expand Up @@ -42,3 +42,45 @@ def test_get_correct_config(self):
)
config = self.version.config
self.assertEqual(config, {'version': 2})

def test_get_correct_config_when_same_config(self):
build_old = get(
Build,
project=self.project,
version=self.version,
config={}
)
build_old.config = {'version': 1}
build_old.save()

build_new = get(
Build,
project=self.project,
version=self.version,
config={}
)
build_new.config = {'version': 1}
build_new.save()

build_new_error = get(
Build,
project=self.project,
version=self.version,
config={},
success=False,
)
build_new_error.config = {'version': 3}
build_new_error.save()

build_new_unfinish = get(
Build,
project=self.project,
version=self.version,
config={},
state='building',
)
build_new_unfinish.config = {'version': 1}
build_new_unfinish.save()

config = self.version.config
self.assertEqual(config, {'version': 1})