Skip to content

Async github/bitbucket repository syncing. #1417

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

Merged
merged 27 commits into from
Aug 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
389742d
Moving Github/Bitbucket object create logic from helper functions int…
gregmuellegger Jul 9, 2015
2f3f1e1
Add PublicTask as base for monitorable celery tasks
gregmuellegger Jul 17, 2015
abbcf34
Add decorator for assigning reusable permission checks on the task
gregmuellegger Jul 17, 2015
6eb47c8
Add helper for retrieving task data from celery
gregmuellegger Jul 17, 2015
1772864
Add interface to retrieve task data that the request is authorized for
gregmuellegger Jul 17, 2015
83380e7
Add reusable user_id_matches permission check for public tasks
gregmuellegger Jul 17, 2015
938a3f4
Add api v2 endpoint to query celery task status
gregmuellegger Jul 17, 2015
21a7d7b
Add api endpoints to trigger async github/bitbucket repo syncing
gregmuellegger Jul 17, 2015
37e2689
Add django-csrf.js to support csrf protected POST requests.
gregmuellegger Jul 17, 2015
8ebec2e
Make github/bitbucket repo syncing async in the frontend
gregmuellegger Jul 17, 2015
b80c171
Remove tests for github/bitbucket sync views
gregmuellegger Jul 17, 2015
e0b2060
Disable task status updates for PublicTask when CELERY_ALWAYS_EAGER
gregmuellegger Jul 20, 2015
bb04d9f
Show sync error when status update request fails on repo import page
gregmuellegger Jul 20, 2015
db667ab
Add api endpoints to trigger async github/bitbucket repo syncing
gregmuellegger Jul 17, 2015
fc6a4ca
Make core.utils a package
gregmuellegger Jul 20, 2015
71f7e91
Move rtd.utils.tasks into core.utils.tasks
gregmuellegger Jul 20, 2015
b41c9a6
Move django-csrf.js and rtd-import.js into core's static-src directory
gregmuellegger Jul 20, 2015
85eb3fe
Update assets build
gregmuellegger Jul 20, 2015
2487c39
Move common JS imports for project import pages into base template
gregmuellegger Jul 20, 2015
9ed0fe7
Rebuilding JS files
gregmuellegger Jul 27, 2015
1b3373e
Update projectimport.js to use the gulp buildsystem
gregmuellegger Jul 27, 2015
0b9b520
Fix typo in data-target for repolist updates
gregmuellegger Jul 27, 2015
b3a867e
Prepend imports with 'readthedocs.'
gregmuellegger Aug 6, 2015
976d117
Remove not implemented run_public stub method from PublicTask
gregmuellegger Aug 6, 2015
215e4dd
Get repos for the user’s BB name, not their RTD name :)
ericholscher Aug 6, 2015
a6eb4bd
Fix import paths on URLs
ericholscher Aug 6, 2015
417b1d5
Show friendlier message when there are no repos.
ericholscher Aug 6, 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
6 changes: 5 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ var gulp = require('gulp'),
// picking up dependencies of the primary entry points and putting any
// limitations on directory structure for entry points.
var sources = {
core: ['js/readthedocs-doc-embed.js', 'js/autocomplete.js'],
core: [
'js/readthedocs-doc-embed.js',
'js/autocomplete.js',
'js/projectimport.js',
],
projects: ['js/tools.js'],
gold: ['js/gold.js'],
donate: ['js/donate.js']
Expand Down
2 changes: 2 additions & 0 deletions media/css/core.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ input[type="hidden"] { display: none; }
input[type="checkbox"], input[type="radio"] { display: inline; }
label { display: block; margin-bottom: 4px; font-weight: bold; color: #444; }

input[type="submit"].inline, input[type="button"].inline, button.inline, .button.inline { display: inline; }

h2 > span.link-help,
h3 > span.link-help,
label > span.link-help {
Expand Down
36 changes: 0 additions & 36 deletions media/javascript/rtd-import.js

This file was deleted.

30 changes: 30 additions & 0 deletions readthedocs/core/static-src/core/js/django-csrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}


$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');

xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
100 changes: 100 additions & 0 deletions readthedocs/core/static-src/core/js/projectimport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
require('./django-csrf.js');


$(function() {
var input = $('#id_repo'),
repo = $('#id_repo_type');

input.blur(function () {
var val = input.val(),
type;

switch(true) {
case /^hg/.test(val):
type = 'hg';
break;

case /^bzr/.test(val):
case /launchpad/.test(val):
type = 'bzr';
break;

case /trunk/.test(val):
case /^svn/.test(val):
type = 'svn';
break;

default:
case /github/.test(val):
case /(^git|\.git$)/.test(val):
type = 'git';
break;
}

repo.val(type);
});

$('[data-sync-repositories]').each(function () {
var $button = $(this);
var target = $(this).attr('data-target');

$button.on('click', function () {
var url = $button.attr('data-sync-repositories');
$.ajax({
method: 'POST',
url: url,
success: function (data) {
$button.attr('disabled', true);
watchProgress(data.url);
},
error: function () {
onError();
}
});
$('.sync-repositories').addClass('hide');
$('.sync-repositories-progress').removeClass('hide');
});

function watchProgress(url) {
setTimeout(function () {
$.ajax({
method: 'GET',
url: url,
success: function (data) {
if (data.finished) {
if (data.success) {
onSuccess();
} else {
onError();
}
} else {
watchProgress(url);
}
},
error: onError
});
}, 2000);
}

function onSuccess(url) {
$.ajax({
method: 'GET',
url: window.location.href,
success: function (data) {
var $newContent = $(data).find(target);
$('body').find(target).replaceWith($newContent);
$('.sync-repositories').addClass('hide');
$('.sync-repositories-progress').addClass('hide');
$('.sync-repositories-success').removeClass('hide');
},
error: onError
});
}

function onError() {
$('.sync-repositories').addClass('hide');
$('.sync-repositories-progress').addClass('hide');
$('.sync-repositories-error').removeClass('hide');
}
});
});
1 change: 1 addition & 0 deletions readthedocs/core/static/core/js/projectimport.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
3 changes: 3 additions & 0 deletions readthedocs/core/utils/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .permission_checks import *
from .public import *
from .retrieve import *
9 changes: 9 additions & 0 deletions readthedocs/core/utils/tasks/permission_checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__all__ = ('user_id_matches',)


def user_id_matches(request, state, context):
Copy link
Member

Choose a reason for hiding this comment

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

We have some logic around this kind of stuff in the privacy app as well -- so it might make sense to put it there.

user_id = context.get('user_id', None)
if user_id is not None and request.user.is_authenticated():
if request.user.id == user_id:
return True
return False
122 changes: 122 additions & 0 deletions readthedocs/core/utils/tasks/public.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from celery import Task
from django.conf import settings

from .retrieve import TaskNotFound
from .retrieve import get_task_data


__all__ = (
'PublicTask', 'TaskNoPermission', 'permission_check',
'get_public_task_data')


STATUS_UPDATES_ENABLED = not getattr(settings, 'CELERY_ALWAYS_EAGER', False)


class PublicTask(Task):
"""
See oauth.tasks for usage example.

Subclasses need to define a ``run_public`` method.
"""
public_name = 'unknown'

@classmethod
def check_permission(cls, request, state, context):
"""
Override this method to define who can monitor this task.
"""
return False

def get_task_data(self):
"""
Return a tuple with the state that should be set next and the results
task.
"""
state = 'STARTED'
info = {
'task_name': self.name,
'context': self.request.get('permission_context', {}),
'public_data': self.request.get('public_data', {}),
}
return state, info

def update_progress_data(self):
state, info = self.get_task_data()
if STATUS_UPDATES_ENABLED:
self.update_state(state=state, meta=info)

def set_permission_context(self, context):
"""
Set data that can be used by ``check_permission`` to authorize a
request for the this task. By default it will be the ``kwargs`` passed
into the task.
"""
self.request.update(permission_context=context)
self.update_progress_data()

def set_public_data(self, data):
"""
Set data that can be displayed in the frontend to authorized users.
This might include progress data about the task.
"""
self.request.update(public_data=data)
self.update_progress_data()

def run(self, *args, **kwargs):
self.set_permission_context(kwargs)
result = self.run_public(*args, **kwargs)
if result is not None:
self.set_public_data(result)
state, info = self.get_task_data()
return info


def permission_check(check):
"""
Class decorator for subclasses of PublicTask to sprinkle in re-usable
permission checks::

@permission_check(user_id_matches)
class MyTask(PublicTask):
def run_public(self, user_id):
pass
"""

def decorator(cls):
cls.check_permission = staticmethod(check)
return cls
return decorator


class TaskNoPermission(Exception):
def __init__(self, task_id, *args, **kwargs):
message = 'No permission to access task with id {id}'.format(
id=task_id)
super(TaskNoPermission, self).__init__(message, *args, **kwargs)


def get_public_task_data(request, task_id):
"""
Return a 3-value tuple with the public name of the task, the current state
of the task and the data that can be displayed publicly about this task.

Will raise `TaskNoPermission` if `request` has no permission to access info
of the task with id `task_id`. This is also the case of no task with the
given id exists.
"""
try:
task, state, info = get_task_data(task_id)
except TaskNotFound:
# No task info has been found act like we don't have permission to see
# the results.
raise TaskNoPermission(task_id)

if not hasattr(task, 'check_permission'):
raise TaskNoPermission(task_id)

context = info.get('context', {})
if not task.check_permission(request, state, context):
raise TaskNoPermission(task_id)
public_name = task.public_name
return public_name, state, info.get('public_data', {})
30 changes: 30 additions & 0 deletions readthedocs/core/utils/tasks/retrieve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from djcelery import celery as celery_app
from celery.result import AsyncResult


__all__ = ('TaskNotFound', 'get_task_data')


class TaskNotFound(Exception):
def __init__(self, task_id, *args, **kwargs):
message = 'No public task found with id {id}'.format(id=task_id)
super(TaskNotFound, self).__init__(message, *args, **kwargs)


def get_task_data(task_id):
"""
Will raise `TaskNotFound` if the task is in state ``PENDING`` or the task
meta data has no ``'task_name'`` key set.
"""

result = AsyncResult(task_id)
state, info = result.state, result.info
if state == 'PENDING':
raise TaskNotFound(task_id)
if 'task_name' not in info:
raise TaskNotFound(task_id)
try:
task = celery_app.tasks[info['task_name']]
except KeyError:
raise TaskNotFound(task_id)
return task, state, info
Loading