diff --git a/gulpfile.js b/gulpfile.js index 5ddd7c2e38a..8964d6bdb77 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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'] diff --git a/media/css/core.css b/media/css/core.css index 537afa3feec..7bee0e81a6b 100644 --- a/media/css/core.css +++ b/media/css/core.css @@ -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 { diff --git a/media/javascript/rtd-import.js b/media/javascript/rtd-import.js deleted file mode 100644 index 5b297be66f5..00000000000 --- a/media/javascript/rtd-import.js +++ /dev/null @@ -1,36 +0,0 @@ -(function () { - $(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); - }); - }); - -})(); diff --git a/readthedocs/core/static-src/core/js/django-csrf.js b/readthedocs/core/static-src/core/js/django-csrf.js new file mode 100644 index 00000000000..5bc24664bca --- /dev/null +++ b/readthedocs/core/static-src/core/js/django-csrf.js @@ -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); + } + } +}); diff --git a/readthedocs/core/static-src/core/js/projectimport.js b/readthedocs/core/static-src/core/js/projectimport.js new file mode 100644 index 00000000000..22067f368d1 --- /dev/null +++ b/readthedocs/core/static-src/core/js/projectimport.js @@ -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'); + } + }); +}); diff --git a/readthedocs/core/static/core/js/projectimport.js b/readthedocs/core/static/core/js/projectimport.js new file mode 100644 index 00000000000..604eeb1c83d --- /dev/null +++ b/readthedocs/core/static/core/js/projectimport.js @@ -0,0 +1 @@ +!function e(r,s,t){function o(i,a){if(!s[i]){if(!r[i]){var c="function"==typeof require&&require;if(!a&&c)return c(i,!0);if(n)return n(i,!0);var u=new Error("Cannot find module '"+i+"'");throw u.code="MODULE_NOT_FOUND",u}var d=s[i]={exports:{}};r[i][0].call(d.exports,function(e){var s=r[i][1][e];return o(s?s:e)},d,d.exports,e,r,s,t)}return s[i].exports}for(var n="function"==typeof require&&require,i=0;i[-\w]+)/$', 'readthedocs.projects.views.private.project_manage', name='projects_manage'), diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 40c4784a2ad..d05bfa27a68 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -600,9 +600,10 @@ def project_redirects_delete(request, project_slug): @login_required -def project_import_github(request, sync=False): +def project_import_github(request): '''Show form that prefills import form with data from GitHub''' - github_connected = oauth_utils.import_github(user=request.user, sync=sync) + github_connected = oauth_utils.import_github( + user=request.user, sync=False) repos = GithubProject.objects.filter(users__in=[request.user]) # Find existing projects that match a repo url @@ -623,17 +624,17 @@ def project_import_github(request, sync=False): { 'repos': repos, 'github_connected': github_connected, - 'sync': sync, }, context_instance=RequestContext(request) ) @login_required -def project_import_bitbucket(request, sync=False): +def project_import_bitbucket(request): '''Show form that prefills import form with data from BitBucket''' - bitbucket_connected = oauth_utils.import_bitbucket(user=request.user, sync=sync) + bitbucket_connected = oauth_utils.import_bitbucket( + user=request.user, sync=False) repos = BitbucketProject.objects.filter(users__in=[request.user]) # Find existing projects that match a repo url @@ -654,7 +655,6 @@ def project_import_bitbucket(request, sync=False): { 'repos': repos, 'bitbucket_connected': bitbucket_connected, - 'sync': sync, }, context_instance=RequestContext(request) ) diff --git a/readthedocs/restapi/urls.py b/readthedocs/restapi/urls.py index 1aa21988166..23cddeb4fd2 100644 --- a/readthedocs/restapi/urls.py +++ b/readthedocs/restapi/urls.py @@ -29,4 +29,13 @@ url(r'search/section/$', 'readthedocs.restapi.views.search_views.section_search', name='api_section_search'), + url(r'jobs/status/(?P[^/]+)/', + 'readthedocs.restapi.views.task_views.job_status', + name='api_job_status'), + url(r'jobs/sync-github-repositories/', + 'readthedocs.restapi.views.task_views.sync_github_repositories', + name='api_sync_github_repositories'), + url(r'jobs/sync-bitbucket-repositories/', + 'readthedocs.restapi.views.task_views.sync_bitbucket_repositories', + name='api_sync_bitbucket_repositories'), ) diff --git a/readthedocs/restapi/views/task_views.py b/readthedocs/restapi/views/task_views.py new file mode 100644 index 00000000000..dd791e1d405 --- /dev/null +++ b/readthedocs/restapi/views/task_views.py @@ -0,0 +1,71 @@ +import logging + +from django.core.urlresolvers import reverse +from rest_framework import decorators, permissions +from rest_framework.renderers import JSONPRenderer, JSONRenderer, BrowsableAPIRenderer +from rest_framework.response import Response + +from readthedocs.core.utils.tasks import TaskNoPermission +from readthedocs.core.utils.tasks import get_public_task_data +from readthedocs.oauth import tasks + + +log = logging.getLogger(__name__) + + +SUCCESS_STATES = ('SUCCESS',) +FAILURE_STATES = ('FAILURE', 'REVOKED',) +FINISHED_STATES = SUCCESS_STATES + FAILURE_STATES +STARTED_STATES = ('RECEIVED', 'STARTED', 'RETRY') + FINISHED_STATES + + +def get_status_data(task_name, state, data): + return { + 'name': task_name, + 'data': data, + 'started': state in STARTED_STATES, + 'finished': state in FINISHED_STATES, + 'success': state in SUCCESS_STATES, + } + + +@decorators.api_view(['GET']) +@decorators.permission_classes((permissions.AllowAny,)) +@decorators.renderer_classes( + (JSONRenderer, JSONPRenderer, BrowsableAPIRenderer)) +def job_status(request, task_id): + try: + task_name, state, public_data = get_public_task_data(request, task_id) + except TaskNoPermission: + return Response( + get_status_data('unknown', 'PENDING', {})) + return Response( + get_status_data(task_name, state, public_data)) + + +@decorators.api_view(['POST']) +@decorators.permission_classes((permissions.IsAuthenticated,)) +@decorators.renderer_classes( + (JSONRenderer, JSONPRenderer, BrowsableAPIRenderer)) +def sync_github_repositories(request): + result = tasks.sync_github_repositories.delay( + user_id=request.user.id) + task_id = result.task_id + return Response({ + 'task_id': task_id, + 'url': reverse('api_job_status', kwargs={'task_id': task_id}), + }) + + +@decorators.api_view(['POST']) +@decorators.permission_classes((permissions.IsAuthenticated,)) +@decorators.renderer_classes( + (JSONRenderer, JSONPRenderer, BrowsableAPIRenderer)) +def sync_bitbucket_repositories(request): + result = tasks.sync_bitbucket_repositories.delay( + user_id=request.user.id) + task_id = result.task_id + return Response({ + 'task_id': task_id, + 'url': reverse('api_job_status', kwargs={'task_id': task_id}), + }) diff --git a/readthedocs/rtd_tests/tests/test_oauth.py b/readthedocs/rtd_tests/tests/test_oauth.py index 4e064dc5ee7..62eaa5f86d3 100644 --- a/readthedocs/rtd_tests/tests/test_oauth.py +++ b/readthedocs/rtd_tests/tests/test_oauth.py @@ -5,7 +5,7 @@ from readthedocs.projects.models import Project -from readthedocs.oauth.utils import make_github_project, make_github_organization, import_github +from readthedocs.oauth.utils import import_github from readthedocs.oauth.models import GithubOrganization, GithubProject @@ -30,7 +30,9 @@ def test_make_github_project_pass(self): "ssh_url": "", "html_url": "", } - github_project = make_github_project(user=self.user, org=self.org, privacy=self.privacy, repo_json=repo_json) + github_project = GithubProject.objects.create_from_api( + repo_json, user=self.user, organization=self.org, + privacy=self.privacy) self.assertIsInstance(github_project, GithubProject) def test_make_github_project_fail(self): @@ -43,7 +45,9 @@ def test_make_github_project_fail(self): "ssh_url": "", "html_url": "", } - github_project = make_github_project(user=self.user, org=self.org, privacy=self.privacy, repo_json=repo_json) + github_project = GithubProject.objects.create_from_api( + repo_json, user=self.user, organization=self.org, + privacy=self.privacy) self.assertIsNone(github_project) def test_make_github_organization(self): @@ -53,7 +57,8 @@ def test_make_github_organization(self): "email": "", "login": "", } - org = make_github_organization(self.user, org_json) + org = GithubOrganization.objects.create_from_api( + org_json, user=self.user) self.assertIsInstance(org, GithubOrganization) def test_import_github_with_no_token(self): @@ -61,6 +66,7 @@ def test_import_github_with_no_token(self): self.assertEqual(github_connected, False) def test_multiple_users_same_repo(self): + user2 = User.objects.get(pk=2) repo_json = { "name": "", "full_name": "testrepo/multiple", @@ -70,21 +76,31 @@ def test_multiple_users_same_repo(self): "ssh_url": "", "html_url": "", } - github_project = make_github_project(user=self.user, org=self.org, privacy=self.privacy, repo_json=repo_json) - github_project_2 = make_github_project(user=User.objects.get(pk=2), org=self.org, privacy=self.privacy, repo_json=repo_json) + + github_project = GithubProject.objects.create_from_api( + repo_json, user=self.user, organization=self.org, + privacy=self.privacy) + github_project_2 = GithubProject.objects.create_from_api( + repo_json, user=user2, organization=self.org, privacy=self.privacy) self.assertIsInstance(github_project, GithubProject) self.assertIsInstance(github_project_2, GithubProject) self.assertNotEqual(github_project_2, github_project) - github_project_3 = make_github_project(user=self.user, org=self.org, privacy=self.privacy, repo_json=repo_json) - github_project_4 = make_github_project(user=User.objects.get(pk=2), org=self.org, privacy=self.privacy, repo_json=repo_json) + github_project_3 = GithubProject.objects.create_from_api( + repo_json, user=self.user, organization=self.org, + privacy=self.privacy) + github_project_4 = GithubProject.objects.create_from_api( + repo_json, user=user2, organization=self.org, privacy=self.privacy) self.assertIsInstance(github_project_3, GithubProject) self.assertIsInstance(github_project_4, GithubProject) self.assertEqual(github_project, github_project_3) self.assertEqual(github_project_2, github_project_4) - github_project_5 = make_github_project(user=self.user, org=self.org, privacy=self.privacy, repo_json=repo_json) - github_project_6 = make_github_project(user=User.objects.get(pk=2), org=self.org, privacy=self.privacy, repo_json=repo_json) + github_project_5 = GithubProject.objects.create_from_api( + repo_json, user=self.user, organization=self.org, + privacy=self.privacy) + github_project_6 = GithubProject.objects.create_from_api( + repo_json, user=user2, organization=self.org, privacy=self.privacy) self.assertEqual(github_project, github_project_5) self.assertEqual(github_project_2, github_project_6) diff --git a/readthedocs/rtd_tests/tests/test_views.py b/readthedocs/rtd_tests/tests/test_views.py index 5e645fbd18a..89c2b21223e 100644 --- a/readthedocs/rtd_tests/tests/test_views.py +++ b/readthedocs/rtd_tests/tests/test_views.py @@ -82,16 +82,10 @@ def test_import_github(self): response = self.client.get('/dashboard/import/github/') self.assertRedirectToLogin(response) - response = self.client.get('/dashboard/import/github/sync/') - self.assertRedirectToLogin(response) - def test_import_bitbucket(self): response = self.client.get('/dashboard/import/bitbucket/') self.assertRedirectToLogin(response) - response = self.client.get('/dashboard/import/bitbucket/sync/') - self.assertRedirectToLogin(response) - def test_projects_manage(self): response = self.client.get('/dashboard/pip/') self.assertRedirectToLogin(response) diff --git a/readthedocs/templates/projects/project_import_bitbucket.html b/readthedocs/templates/projects/project_import_bitbucket.html index 47933d23d25..07043eac2c6 100644 --- a/readthedocs/templates/projects/project_import_bitbucket.html +++ b/readthedocs/templates/projects/project_import_bitbucket.html @@ -1,39 +1,27 @@ -{% extends "base.html" %} +{% extends "projects/project_import_from_service.html" %} {% load i18n %} -{% block title %}{% trans "Import a BitBucket project" %}{% endblock %} - -{% block extra_links %} - - -{% endblock %} -{% block extra_scripts %} - - - - -{% endblock %} +{% block title %}{% trans "Import a BitBucket project" %}{% endblock %} {% block content-header %}

{% trans "Import a BitBucket project" %}

{% endblock %} {% block content %} {% if bitbucket_connected %} -{% if not sync %} -

- {% url 'projects_sync_bitbucket' as sync_bitbucket_url %} - {% blocktrans %}Showing your BitBucket projects. Sync your BitBucket projects to update them.{% endblocktrans %} +

+ {% url 'api_sync_bitbucket_repositories' as sync_url %} + {% blocktrans with button_attrs=' class="inline" data-sync-repositories="'|add:sync_url|add:'" data-target=".repository-list"'|safe %}Showing your BitBucket projects. to update them.{% endblocktrans %}

-{% else %} -

+

+ {% blocktrans %}Syncing ...{% endblocktrans %} +

+

{% blocktrans %}BitBucket projects are now up to date.{% endblocktrans %}

-{% endif %} -
+

+ {% blocktrans %}Your BitBucket projects could not be synced. Please try again later.{% endblocktrans %} +

+
@@ -74,6 +62,12 @@ + {% empty %} +
  • +

    + You don't have any BitBucket repositories currently. +

    +
  • {% endfor %}
    @@ -82,7 +76,7 @@
    {% else %} {% url 'socialaccount_connections' as social_url %} - {% blocktrans %}You don't have any BitBucket repositories connected. + {% blocktrans %}You don't have any BitBucket repositories connected. Go to your Social Accounts to set one up. {% endblocktrans %} {% endif %} diff --git a/readthedocs/templates/projects/project_import_from_service.html b/readthedocs/templates/projects/project_import_from_service.html new file mode 100644 index 00000000000..0bdb520f75d --- /dev/null +++ b/readthedocs/templates/projects/project_import_from_service.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load static %} + + +{% block extra_links %} + + +{% endblock %} + +{% block extra_scripts %} + + + + +{% endblock %} diff --git a/readthedocs/templates/projects/project_import_github.html b/readthedocs/templates/projects/project_import_github.html index 77e6c757358..45d640c1a19 100644 --- a/readthedocs/templates/projects/project_import_github.html +++ b/readthedocs/templates/projects/project_import_github.html @@ -1,39 +1,27 @@ -{% extends "base.html" %} +{% extends "projects/project_import_from_service.html" %} {% load i18n %} -{% block title %}{% trans "Import a GitHub project" %}{% endblock %} - -{% block extra_links %} - - -{% endblock %} -{% block extra_scripts %} - - - - -{% endblock %} +{% block title %}{% trans "Import a GitHub project" %}{% endblock %} {% block content-header %}

    {% trans "Import a GitHub project" %}

    {% endblock %} {% block content %} {% if github_connected %} -{% if not sync %} -

    - {% url 'projects_sync_github' as sync_github_url %} - {% blocktrans %}Showing your GitHub projects. Sync your GitHub projects to update them.{% endblocktrans %} +

    + {% url 'api_sync_github_repositories' as sync_url %} + {% blocktrans with button_attrs=' class="inline" data-sync-repositories="'|add:sync_url|add:'" data-target=".repository-list"'|safe %}Showing your GitHub projects. to update them.{% endblocktrans %}

    -{% else %} -

    +

    + {% blocktrans %}Syncing ...{% endblocktrans %} +

    +

    {% blocktrans %}GitHub projects are now up to date.{% endblocktrans %}

    -{% endif %} -
    +

    + {% blocktrans %}Your GitHub projects could not be synced. Please try again later.{% endblocktrans %} +

    +
    @@ -74,6 +62,12 @@ + {% empty %} +
  • +

    + You don't have any GitHub repositories currently. +

    +
  • {% endfor %}