Skip to content

Commit 457823d

Browse files
committed
Merge pull request #1563 from rtfd/build-ux-refactor
Add per-command tracking to API out output detail page
2 parents b0d4e39 + 5a44915 commit 457823d

File tree

23 files changed

+850
-233
lines changed

23 files changed

+850
-233
lines changed

gulpfile.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var gulp = require('gulp'),
1717
// picking up dependencies of the primary entry points and putting any
1818
// limitations on directory structure for entry points.
1919
var sources = {
20+
builds: ['js/detail.js'],
2021
core: [
2122
'js/readthedocs-doc-embed.js',
2223
'js/autocomplete.js',

readthedocs/api/base.py

-35
Original file line numberDiff line numberDiff line change
@@ -189,41 +189,6 @@ def override_urls(self):
189189
]
190190

191191

192-
class BuildResource(ModelResource):
193-
project = fields.ForeignKey('readthedocs.api.base.ProjectResource', 'project')
194-
version = fields.ForeignKey('readthedocs.api.base.VersionResource', 'version')
195-
196-
class Meta(object):
197-
always_return_data = True
198-
include_absolute_url = True
199-
allowed_methods = ['get', 'post', 'put']
200-
queryset = Build.objects.api()
201-
authentication = PostAuthentication()
202-
authorization = DjangoAuthorization()
203-
filtering = {
204-
"project": ALL_WITH_RELATIONS,
205-
"slug": ALL_WITH_RELATIONS,
206-
"type": ALL_WITH_RELATIONS,
207-
"state": ALL_WITH_RELATIONS,
208-
}
209-
210-
def get_object_list(self, request):
211-
self._meta.queryset = Build.objects.api(user=request.user)
212-
return super(BuildResource, self).get_object_list(request)
213-
214-
def override_urls(self):
215-
return [
216-
url(r"^(?P<resource_name>%s)/schema/$"
217-
% self._meta.resource_name,
218-
self.wrap_view('get_schema'),
219-
name="api_get_schema"),
220-
url(r"^(?P<resource_name>%s)/(?P<project__slug>[a-z-_]+)/$" %
221-
self._meta.resource_name,
222-
self.wrap_view('dispatch_list'),
223-
name="build_list_detail"),
224-
]
225-
226-
227192
class FileResource(ModelResource, SearchMixin):
228193
project = fields.ForeignKey(ProjectResource, 'project', full=True)
229194

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
import readthedocs.builds.models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('builds', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='BuildCommandResult',
17+
fields=[
18+
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19+
('command', models.TextField(verbose_name='Command')),
20+
('description', models.TextField(verbose_name='Description', blank=True)),
21+
('output', models.TextField(verbose_name='Command output', blank=True)),
22+
('exit_code', models.IntegerField(verbose_name='Command exit code')),
23+
('start_time', models.DateTimeField(verbose_name='Start time')),
24+
('end_time', models.DateTimeField(verbose_name='End time')),
25+
('build', models.ForeignKey(related_name='commands', verbose_name='Build', to='builds.Build')),
26+
],
27+
options={
28+
'ordering': ['start_time'],
29+
'get_latest_by': 'start_time',
30+
},
31+
bases=(readthedocs.builds.models.BuildCommandResultMixin, models.Model),
32+
),
33+
]

readthedocs/builds/models.py

+59-7
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from guardian.shortcuts import assign
1212
from taggit.managers import TaggableManager
1313

14-
from readthedocs.privacy.loader import VersionManager, RelatedProjectManager
14+
from readthedocs.privacy.loader import (VersionManager, RelatedProjectManager,
15+
RelatedBuildManager)
1516
from readthedocs.projects.models import Project
16-
from readthedocs.projects import constants
17-
from .constants import (BUILD_STATE, BUILD_TYPES, VERSION_TYPES,
18-
LATEST, NON_REPOSITORY_VERSIONS, STABLE
19-
)
17+
from readthedocs.projects.constants import (PRIVACY_CHOICES, REPO_TYPE_GIT,
18+
REPO_TYPE_HG)
2019

20+
from .constants import (BUILD_STATE, BUILD_TYPES, VERSION_TYPES,
21+
LATEST, NON_REPOSITORY_VERSIONS, STABLE,
22+
BUILD_STATE_FINISHED)
2123
from .version_slug import VersionSlugField
2224

2325

@@ -70,7 +72,7 @@ class Version(models.Model):
7072
built = models.BooleanField(_('Built'), default=False)
7173
uploaded = models.BooleanField(_('Uploaded'), default=False)
7274
privacy_level = models.CharField(
73-
_('Privacy Level'), max_length=20, choices=constants.PRIVACY_CHOICES,
75+
_('Privacy Level'), max_length=20, choices=PRIVACY_CHOICES,
7476
default=DEFAULT_VERSION_PRIVACY_LEVEL, help_text=_("Level of privacy for this Version.")
7577
)
7678
tags = TaggableManager(blank=True)
@@ -368,4 +370,54 @@ def get_absolute_url(self):
368370
@property
369371
def finished(self):
370372
'''Return if build has a finished state'''
371-
return self.state == 'finished'
373+
return self.state == BUILD_STATE_FINISHED
374+
375+
376+
class BuildCommandResultMixin(object):
377+
'''Mixin for common command result methods/properties
378+
379+
Shared methods between the database model :py:cls:`BuildCommandResult` and
380+
non-model respresentations of build command results from the API
381+
'''
382+
383+
@property
384+
def successful(self):
385+
'''Did the command exit with a successful exit code'''
386+
return self.exit_code == 0
387+
388+
@property
389+
def failed(self):
390+
'''Did the command exit with a failing exit code
391+
392+
Helper for inverse of :py:meth:`successful`'''
393+
return not self.successful
394+
395+
396+
class BuildCommandResult(BuildCommandResultMixin, models.Model):
397+
build = models.ForeignKey(Build, verbose_name=_('Build'),
398+
related_name='commands')
399+
400+
command = models.TextField(_('Command'))
401+
description = models.TextField(_('Description'), blank=True)
402+
output = models.TextField(_('Command output'), blank=True)
403+
exit_code = models.IntegerField(_('Command exit code'))
404+
405+
start_time = models.DateTimeField(_('Start time'))
406+
end_time = models.DateTimeField(_('End time'))
407+
408+
class Meta:
409+
ordering = ['start_time']
410+
get_latest_by = 'start_time'
411+
412+
objects = RelatedBuildManager()
413+
414+
def __unicode__(self):
415+
return (ugettext(u'Build command {pk} for build {build}')
416+
.format(pk=self.pk, build=self.build))
417+
418+
@property
419+
def run_time(self):
420+
"""Total command runtime in seconds"""
421+
if self.start_time is not None and self.end_time is not None:
422+
diff = self.end_time - self.start_time
423+
return diff.seconds
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Build detail view
2+
3+
var ko = window.knockout || require('knockout');
4+
var $ = window.jquery || require('jquery');
5+
6+
7+
function BuildCommand (data) {
8+
var self = this;
9+
self.id = ko.observable(data.id);
10+
self.command = ko.observable(data.command);
11+
self.output = ko.observable(data.output);
12+
self.exit_code = ko.observable(data.exit_code || 0);
13+
self.successful = ko.observable(self.exit_code() == 0);
14+
self.run_time = ko.observable(data.run_time);
15+
self.is_showing = ko.observable(!self.successful());
16+
17+
self.toggleCommand = function () {
18+
self.is_showing(!self.is_showing());
19+
};
20+
21+
self.command_status = ko.computed(function () {
22+
return self.successful() ?
23+
'build-command-successful' :
24+
'build-command-failed';
25+
});
26+
}
27+
28+
function BuildDetailView (instance) {
29+
var self = this,
30+
instance = instance || {};
31+
32+
/* Instance variables */
33+
self.state = ko.observable(instance.state);
34+
self.state_display = ko.observable(instance.state_display);
35+
self.finished = ko.computed(function () {
36+
return self.state() == 'finished';
37+
});
38+
self.date = ko.observable(instance.date);
39+
self.success = ko.observable(instance.success);
40+
self.error = ko.observable(instance.error);
41+
self.length = ko.observable(instance.length);
42+
self.commands = ko.observableArray(instance.commands);
43+
self.display_commands = ko.computed(function () {
44+
var commands_display = [],
45+
commands_raw = self.commands();
46+
for (n in commands_raw) {
47+
var command = new BuildCommand(commands_raw[n]);
48+
commands_display.push(command)
49+
}
50+
return commands_display;
51+
});
52+
self.commit = ko.observable(instance.commit);
53+
54+
/* Others */
55+
self.legacy_output = ko.observable(false);
56+
self.show_legacy_output = function () {
57+
self.legacy_output(true);
58+
};
59+
60+
function poll_api () {
61+
if (self.finished()) {
62+
return;
63+
}
64+
$.getJSON('/api/v2/build/' + instance.id + '/', function (data) {
65+
self.state(data.state);
66+
self.state_display(data.state_display);
67+
self.date(data.date);
68+
self.success(data.success);
69+
self.error(data.error);
70+
self.length(data.length);
71+
self.commit(data.commit);
72+
for (n in data.commands) {
73+
var command = data.commands[n];
74+
var match = ko.utils.arrayFirst(
75+
self.commands(),
76+
function(command_cmp) {
77+
return (command_cmp.id == command.id);
78+
}
79+
);
80+
if (!match) {
81+
self.commands.push(command);
82+
}
83+
}
84+
});
85+
86+
setTimeout(poll_api, 2000);
87+
}
88+
89+
poll_api();
90+
}
91+
92+
BuildDetailView.init = function (instance, domobj) {
93+
var view = new BuildDetailView(instance),
94+
domobj = domobj || $('#build-detail')[0];
95+
ko.applyBindings(view, domobj);
96+
return view;
97+
};
98+
99+
module.exports.BuildDetailView = BuildDetailView;
100+
101+
if (typeof(window) != 'undefined') {
102+
window.build = module.exports;
103+
}

0 commit comments

Comments
 (0)