-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
+620
−198
Merged
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 2f3f1e1
Add PublicTask as base for monitorable celery tasks
gregmuellegger abbcf34
Add decorator for assigning reusable permission checks on the task
gregmuellegger 6eb47c8
Add helper for retrieving task data from celery
gregmuellegger 1772864
Add interface to retrieve task data that the request is authorized for
gregmuellegger 83380e7
Add reusable user_id_matches permission check for public tasks
gregmuellegger 938a3f4
Add api v2 endpoint to query celery task status
gregmuellegger 21a7d7b
Add api endpoints to trigger async github/bitbucket repo syncing
gregmuellegger 37e2689
Add django-csrf.js to support csrf protected POST requests.
gregmuellegger 8ebec2e
Make github/bitbucket repo syncing async in the frontend
gregmuellegger b80c171
Remove tests for github/bitbucket sync views
gregmuellegger e0b2060
Disable task status updates for PublicTask when CELERY_ALWAYS_EAGER
gregmuellegger bb04d9f
Show sync error when status update request fails on repo import page
gregmuellegger db667ab
Add api endpoints to trigger async github/bitbucket repo syncing
gregmuellegger fc6a4ca
Make core.utils a package
gregmuellegger 71f7e91
Move rtd.utils.tasks into core.utils.tasks
gregmuellegger b41c9a6
Move django-csrf.js and rtd-import.js into core's static-src directory
gregmuellegger 85eb3fe
Update assets build
gregmuellegger 2487c39
Move common JS imports for project import pages into base template
gregmuellegger 9ed0fe7
Rebuilding JS files
gregmuellegger 1b3373e
Update projectimport.js to use the gulp buildsystem
gregmuellegger 0b9b520
Fix typo in data-target for repolist updates
gregmuellegger b3a867e
Prepend imports with 'readthedocs.'
gregmuellegger 976d117
Remove not implemented run_public stub method from PublicTask
gregmuellegger 215e4dd
Get repos for the user’s BB name, not their RTD name :)
ericholscher a6eb4bd
Fix import paths on URLs
ericholscher 417b1d5
Show friendlier message when there are no repos.
ericholscher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
}); | ||
}); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .permission_checks import * | ||
from .public import * | ||
from .retrieve import * |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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): | ||
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', {}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.