From 790cf91e9fc134c40cfffc9ebc40aa0c21a6596e Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Thu, 3 Sep 2015 20:59:37 -0700 Subject: [PATCH 01/52] Allow public repositories to show when default privacy is private --- readthedocs/oauth/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/oauth/managers.py b/readthedocs/oauth/managers.py index 78a7639a82f..f3e7c551fbf 100644 --- a/readthedocs/oauth/managers.py +++ b/readthedocs/oauth/managers.py @@ -15,7 +15,7 @@ def create_from_api(self, api_json, user, organization=None, privacy=DEFAULT_PRIVACY_LEVEL): logger.info('Trying GitHub: %s' % api_json['full_name']) if ( - (api_json['private'] is True and privacy == 'private') or + (privacy == 'private') or (api_json['private'] is False and privacy == 'public')): project, created = self.get_or_create( full_name=api_json['full_name'], From 383812e4749b0010e7786cfbae4235c89013613f Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Tue, 15 Sep 2015 14:38:13 -0700 Subject: [PATCH 02/52] Somewhat working example of API driven list --- .../core/static-src/core/js/projectimport.js | 204 ++++++++++++++---- readthedocs/core/static-src/core/js/tasks.js | 61 ++++++ .../core/static/core/js/projectimport.js | 2 +- readthedocs/oauth/models.py | 34 ++- readthedocs/restapi/serializers.py | 38 ++++ readthedocs/restapi/urls.py | 8 +- readthedocs/restapi/views/model_views.py | 52 ++++- .../projects/project_import_from_service.html | 37 +++- .../projects/project_import_github.html | 170 ++++++++------- 9 files changed, 459 insertions(+), 147 deletions(-) create mode 100644 readthedocs/core/static-src/core/js/tasks.js diff --git a/readthedocs/core/static-src/core/js/projectimport.js b/readthedocs/core/static-src/core/js/projectimport.js index 22067f368d1..a95b85b36a7 100644 --- a/readthedocs/core/static-src/core/js/projectimport.js +++ b/readthedocs/core/static-src/core/js/projectimport.js @@ -1,3 +1,7 @@ +var ko = require('knockout'), + $ = require('jquery'), + tasks = require('./tasks'); + require('./django-csrf.js'); @@ -35,46 +39,6 @@ $(function() { }); $('[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({ @@ -90,11 +54,159 @@ $(function() { error: onError }); } - - function onError() { - $('.sync-repositories').addClass('hide'); - $('.sync-repositories-progress').addClass('hide'); - $('.sync-repositories-error').removeClass('hide'); - } }); }); + +function Organization (instance) { + var self = this; + self.id = ko.observable(instance.id); + self.name = ko.observable(instance.name); + self.login = ko.observable(instance.login); + self.active = ko.observable(instance.active); + self.avatar_url = ko.observable( + append_url_params(instance.avatar_url, {size: 32}) + ); + self.display_name = ko.computed(function () { + return self.name() || self.login(); + }); +} + +function Owner (instance) { + var self = this; + self.name = ko.observable(instance.name); + self.avatar_url = ko.observable( + append_url_params(instance.avatar_url, {size: 32}) + ); + self.login = ko.observable(instance.login); +} + +function Project (instance) { + var self = this; + self.id = ko.observable(instance.id); + self.name = ko.observable(instance.name); + self.full_name = ko.observable(instance.full_name); + self.organization = ko.observable(); + if (instance.organization) { + self.organization(new Organization(instance.organization)); + } + self.owner = ko.observable(new Owner(instance.owner)); + self.url = ko.observable(instance.url); + self.private = ko.observable(instance.private); + self.active = ko.observable(instance.active); +} + +function ProjectImportView (instance, urls) { + var self = this, + instance = instance || {}; + + self.urls = urls || {}; + + // For task display + self.error = ko.observable(null); + self.is_syncing = ko.observable(false); + + // For filtering + self.page_count = ko.observable(null); + self.page_current = ko.observable(null); + self.page_next = ko.observable(null); + self.page_previous = ko.observable(null); + self.filter_org = ko.observable(null); + + + self.organizations_raw = ko.observableArray(); + self.organizations = ko.computed(function () { + var organizations = [], + organizations_raw = self.organizations_raw(); + for (n in organizations_raw) { + var organization = new Organization(organizations_raw[n]); + organizations.push(organization); + } + return organizations; + }); + self.projects = ko.observableArray() + + ko.computed(function () { + var org = self.filter_org(), + orgs = self.organizations(), + url = self.page_current() || self.urls['githubproject-list']; + + if (org) { + url = append_url_params(url, {org: org}); + } + + $.getJSON(url) + .success(function (projects_list) { + var projects = []; + self.page_next(projects_list.next); + self.page_previous(projects_list.previous); + + for (n in projects_list.results) { + // TODO replace org id here + var project = new Project(projects_list.results[n]); + projects.push(project); + } + self.projects(projects); + }) + .error(function (error) { + self.error(error); + }); + }); + + self.get_organizations = function () { + $.getJSON(self.urls['githuborganization-list']) + .success(function (organizations) { + self.organizations_raw(organizations.results); + }) + .error(function (error) { + self.error(error); + }); + }; + + self.sync_projects = function () { + var url = self.urls.api_sync_github_repositories; + + self.error(null); + self.is_syncing(true); + + tasks.trigger_task(url) + .then(function (data) { + self.get_organizations(); + }) + .fail(function (error) { + error = error || 'An error occured'; + self.error(error); + }) + .always(function () { + self.is_syncing(false); + }) + } + + self.next_page = function () { + self.page_current(self.page_next()); + } + + self.previous_page = function () { + self.page_current(self.page_previous()); + } +} + +function append_url_params (url, params) { + var link = $('').attr('href', url).get(0); + + Object.keys(params).map(function (key) { + if (link.search) { + link.search += '&'; + } + link.search += key + '=' + params[key]; + }); + return link.href; +} + +ProjectImportView.init = function (domobj, instance, urls) { + var view = new ProjectImportView(instance, urls); + view.get_organizations(); + ko.applyBindings(view, domobj); + return view; +}; + +module.exports.ProjectImportView = ProjectImportView; diff --git a/readthedocs/core/static-src/core/js/tasks.js b/readthedocs/core/static-src/core/js/tasks.js new file mode 100644 index 00000000000..c44f60dc102 --- /dev/null +++ b/readthedocs/core/static-src/core/js/tasks.js @@ -0,0 +1,61 @@ +/* Public task tracking */ + +var jquery = require('jquery'); + +function poll_task (data) { + var defer = jquery.Deferred(); + + function poll_task_loop () { + jquery + .getJSON(data.url) + .success(function (task) { + if (task.finished) { + if (task.success) { + defer.resolve(); + } + else { + defer.reject(task.error || 'Error'); + } + } + else { + setTimeout(poll_task_loop, 2000); + } + }) + .error(function (error) { + console.error('Error polling task:', error); + setTimeout(poll_task_loop, 2000); + }); + } + + setTimeout(poll_task_loop, 2000); + + return defer; +} + +function trigger_task (url) { + var defer = jquery.Deferred(); + + $.ajax({ + method: 'POST', + url: url, + success: function (data) { + poll_task(data) + .then(function () { + defer.resolve(); + }) + .fail(function (error) { + defer.reject(error); + }); + }, + error: function (error) { + defer.reject(error); + } + }); + + return defer; +} + +module.exports = { + poll_task: poll_task, + trigger_task: trigger_task +}; diff --git a/readthedocs/core/static/core/js/projectimport.js b/readthedocs/core/static/core/js/projectimport.js index 648d27cc447..5291d852ded 100644 --- a/readthedocs/core/static/core/js/projectimport.js +++ b/readthedocs/core/static/core/js/projectimport.js @@ -1 +1 @@ -require=function e(r,t,s){function o(i,a){if(!t[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=t[i]={exports:{}};r[i][0].call(d.exports,function(e){var t=r[i][1][e];return o(t?t:e)},d,d.exports,e,r,t,s)}return t[i].exports}for(var n="function"==typeof require&&require,i=0;i").attr("href",e).get(0);return Object.keys(r).map(function(e){n.search&&(n.search+="&"),n.search+=e+"="+r[e]}),n.href}var c=e("knockout"),l=e("jquery"),f=e("./tasks");e("./django-csrf.js"),l(function(){var e=l("#id_repo"),r=l("#id_repo_type");e.blur(function(){var n,t=e.val();switch(!0){case/^hg/.test(t):n="hg";break;case/^bzr/.test(t):case/launchpad/.test(t):n="bzr";break;case/trunk/.test(t):case/^svn/.test(t):n="svn";break;default:case/github/.test(t):case/(^git|\.git$)/.test(t):n="git"}r.val(n)}),l("[data-sync-repositories]").each(function(){})}),s.init=function(e,r,n){var t=new s(r,n);return t.get_organizations(),c.applyBindings(t,e),t},r.exports.ProjectImportView=s},{"./django-csrf.js":1,"./tasks":2,jquery:"jquery",knockout:"knockout"}]},{},[]); \ No newline at end of file diff --git a/readthedocs/oauth/models.py b/readthedocs/oauth/models.py index 56c819fae65..61401f99c41 100644 --- a/readthedocs/oauth/models.py +++ b/readthedocs/oauth/models.py @@ -30,6 +30,16 @@ class GithubOrganization(models.Model): def __unicode__(self): return "GitHub Organization: %s" % (self.html_url) + def serialized_field(self, key=None, default=None): + # TODO don't do this with eval! + data = eval(self.json) + if key is not None: + return data.get(key, default) + return data + + def avatar_url(self): + return self.serialized_field('avatar_url') + class GithubProject(models.Model): # Auto fields @@ -53,18 +63,30 @@ class GithubProject(models.Model): objects = GithubProjectManager() + class Meta: + ordering = ['organization__name', 'name'] + def __unicode__(self): return "GitHub Project: %s" % (self.html_url) + def serialized_field(self, key=None, default=None): + # TODO don't do this with eval! + data = eval(self.json) + if key is not None: + return data.get(key, default) + return data + def is_admin(self): - full_json = eval(self.json) - if 'permissions' in full_json: - return full_json['permissions']['admin'] - return False + permissions = self.serialized_field('permissions', {}) + return permissions.get('admin', False) def is_private(self): - full_json = eval(self.json) - return full_json['private'] + return self.serialized_field('private') + + def owner(self): + owner = self.serialized_field('owner', {}) + return dict((key, val) for (key, val) in owner.items() + if key in ['avatar_url', 'login', 'name']) class BitbucketTeam(models.Model): diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index 38e83c8dc13..fb568aaceeb 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -2,6 +2,8 @@ from readthedocs.builds.models import Build, BuildCommandResult, Version from readthedocs.projects.models import Project, Domain +from readthedocs.oauth.models import (GithubOrganization, GithubProject, + BitbucketTeam, BitbucketProject) class ProjectSerializer(serializers.ModelSerializer): @@ -80,3 +82,39 @@ class Meta: 'machine', 'cname', ) + + +class GithubOrganizationSerializer(serializers.ModelSerializer): + + avatar_url = serializers.ReadOnlyField() + + class Meta: + model = GithubOrganization + exclude = ('json', 'email', 'users') + + +class GithubProjectSerializer(serializers.ModelSerializer): + + """Github project serializer""" + + private = serializers.ReadOnlyField(source='is_private') + owner = serializers.ReadOnlyField() + organization = GithubOrganizationSerializer() + + class Meta: + model = GithubProject + exclude = ('json', 'users') + + +class BitbucketTeamSerializer(serializers.ModelSerializer): + + class Meta: + model = BitbucketTeam + exclude = ('json',) + + +class BitbucketProjectSerializer(serializers.ModelSerializer): + + class Meta: + model = BitbucketProject + exclude = ('json',) diff --git a/readthedocs/restapi/urls.py b/readthedocs/restapi/urls.py index deb6c0d1ab4..7355a3dad37 100644 --- a/readthedocs/restapi/urls.py +++ b/readthedocs/restapi/urls.py @@ -4,7 +4,9 @@ from .views.model_views import (BuildViewSet, BuildCommandViewSet, ProjectViewSet, NotificationViewSet, - VersionViewSet, DomainViewSet) + VersionViewSet, DomainViewSet + GithubOrganizationViewSet, GithubProjectViewSet, + BitbucketTeamViewSet, BitbucketProjectViewSet) from readthedocs.comments.views import CommentViewSet router = routers.DefaultRouter() @@ -14,6 +16,10 @@ router.register(r'project', ProjectViewSet) router.register(r'notification', NotificationViewSet) router.register(r'domain', DomainViewSet) +router.register(r'github/org', GithubOrganizationViewSet) +router.register(r'github/project', GithubProjectViewSet) +router.register(r'bitbucket/team', BitbucketTeamViewSet) +router.register(r'bitbucket/project', BitbucketProjectViewSet) router.register(r'comments', CommentViewSet, base_name="comments") urlpatterns = patterns( diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py index 33c11ef1177..a781f87dd59 100644 --- a/readthedocs/restapi/views/model_views.py +++ b/readthedocs/restapi/views/model_views.py @@ -13,6 +13,9 @@ from readthedocs.restapi import utils as api_utils from readthedocs.core.utils import trigger_build from readthedocs.oauth import utils as oauth_utils +from readthedocs.oauth.models import (GithubOrganization, GithubProject, + BitbucketTeam, BitbucketProject) +from readthedocs.builds.constants import STABLE from readthedocs.projects.filters import ProjectFilter, DomainFilter from readthedocs.projects.models import Project, EmailHook, Domain from readthedocs.projects.version_handling import determine_stable_version @@ -21,7 +24,10 @@ RelatedProjectIsOwner) from ..serializers import (BuildSerializerFull, BuildSerializer, BuildCommandSerializer, ProjectSerializer, - VersionSerializer, DomainSerializer) + VersionSerializer, DomainSerializer, + GithubOrganizationSerializer, GithubProjectSerializer, + BitbucketTeamSerializer, BitbucketProjectSerializer) +from .. import utils as api_utils log = logging.getLogger(__name__) @@ -206,3 +212,47 @@ class DomainViewSet(viewsets.ModelViewSet): def get_queryset(self): return self.model.objects.api(self.request.user) + + +class OAuthServiceMixin(object): + def get_queryset(self): + # return self.model.objects.api(self.request.user) + return self.model.objects.filter(users=self.request.user) + + +class GithubOrganizationViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): + permission_classes = [APIPermission] + renderer_classes = [JSONRenderer, BrowsableAPIRenderer] + serializer_class = GithubOrganizationSerializer + model = GithubOrganization + + +class GithubProjectViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): + permission_classes = [APIPermission] + renderer_classes = [JSONRenderer, BrowsableAPIRenderer] + serializer_class = GithubProjectSerializer + model = GithubProject + + def get_queryset(self): + query = super(GithubProjectViewSet, self).get_queryset() + org = self.request.query_params.get('org', None) + if org is not None: + query = query.filter(organization__pk=org) + return query + + def get_paginate_by(self): + return self.request.query_params.get('page_size', 25) + + +class BitbucketProjectViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): + permission_classes = [APIPermission] + renderer_classes = [JSONRenderer, BrowsableAPIRenderer] + serializer_class = BitbucketProjectSerializer + model = BitbucketProject + + +class BitbucketTeamViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): + permission_classes = [APIPermission] + renderer_classes = [JSONRenderer, BrowsableAPIRenderer] + serializer_class = BitbucketTeamSerializer + model = BitbucketTeam diff --git a/readthedocs/templates/projects/project_import_from_service.html b/readthedocs/templates/projects/project_import_from_service.html index 0bdb520f75d..b9c489ec72c 100644 --- a/readthedocs/templates/projects/project_import_from_service.html +++ b/readthedocs/templates/projects/project_import_from_service.html @@ -1,19 +1,34 @@ {% 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 c0d6b68c02a..e5aaf012afa 100644 --- a/readthedocs/templates/projects/project_import_github.html +++ b/readthedocs/templates/projects/project_import_github.html @@ -7,91 +7,99 @@ {% block content-header %}

{% trans "Import a GitHub project" %}

{% endblock %} {% block content %} -{% if github_connected %} - -{% block top_content %} -

- {% 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 %} -

-{% endblock top_content %} - -

- {% blocktrans %}Syncing ...{% endblocktrans %} -

-

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

-

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

- -{% block main_content %} -
-
-
-
-
    - {% for repo in repos %} -
  • - - {{ repo.full_name }} - - {% block form %} -
    - {% csrf_token %} - - - - - - - {% if repo.matches %} - - - {% trans "View Project" %} - - -
      - {% for match in repo.matches %} -
    • {{ match }}
    • - {% endfor %} -
    -
    - - {% else %} -
      - - - -
    - {% endif %} - -
    - {% endblock form %} -
  • - {% empty %} -
  • -

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

    + {% if github_connected %} + + {% block top_content %} + {% url 'api_sync_github_repositories' as sync_url %} + + {% endblock top_content %} + +
      + + +
    • +
    • +
    + + {% block main_content %} +
    + +

    + + Previous + + + Next + +

    + +
    +
    +
    +
    + +
      +
    • + + + + + + + +
        + +
      + +
    • +
    + +
    +
    +
    +
    + +

    + + Previous + + + Next + +

    + +
    + {% endblock main_content %} + + {% block sidebar_content %} +
    +
      +
    • + +
    • - {% endfor %}
    -
-
-
-{% endblock main_content %} + {% endblock %} -{% else %} + {% else %} -{% block empty %} - {% url 'socialaccount_connections' as social_url %} - {% blocktrans %}You don't have any GitHub repositories connected. - Go to your Social Accounts to set one up. - {% endblocktrans %} -{% endblock empty %} + {% block empty %} + {% url 'socialaccount_connections' as social_url %} + {% blocktrans %} + You don't have any GitHub repositories connected. + Go to your Social Accounts to set one up. + {% endblocktrans %} + {% endblock empty %} -{% endif %} + {% endif %} {% endblock %} From f7cf808d7d91e9b595577413b87cbd430a95e387 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 18 Sep 2015 11:52:42 -0700 Subject: [PATCH 03/52] Add OAuth combination models --- readthedocs/oauth/admin.py | 15 ++- readthedocs/oauth/managers.py | 8 ++ .../oauth/migrations/0002_combine_services.py | 60 ++++++++++ readthedocs/oauth/models.py | 111 ++++++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 readthedocs/oauth/migrations/0002_combine_services.py diff --git a/readthedocs/oauth/admin.py b/readthedocs/oauth/admin.py index ad8e86f4749..eadf2b0517f 100644 --- a/readthedocs/oauth/admin.py +++ b/readthedocs/oauth/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin -from .models import GithubProject, GithubOrganization, BitbucketProject, BitbucketTeam + +from .models import (GithubProject, GithubOrganization, BitbucketProject, + BitbucketTeam, OAuthRepository, OAuthOrganization) class GithubProjectAdmin(admin.ModelAdmin): @@ -17,7 +19,18 @@ class BitbucketProjectAdmin(admin.ModelAdmin): class BitBucketTeamAdmin(admin.ModelAdmin): raw_id_fields = ('users',) + +class OAuthRepositoryAdmin(admin.ModelAdmin): + raw_id_fields = ('users',) + + +class OAuthOrganizationAdmin(admin.ModelAdmin): + raw_id_fields = ('users',) + + admin.site.register(GithubOrganization, GithubOrganizationAdmin) admin.site.register(GithubProject, GithubProjectAdmin) admin.site.register(BitbucketTeam, BitBucketTeamAdmin) admin.site.register(BitbucketProject, BitbucketProjectAdmin) +admin.site.register(OAuthRepository, OAuthRepositoryAdmin) +admin.site.register(OAuthOrganization, OAuthOrganizationAdmin) diff --git a/readthedocs/oauth/managers.py b/readthedocs/oauth/managers.py index f3e7c551fbf..0232939d2a9 100644 --- a/readthedocs/oauth/managers.py +++ b/readthedocs/oauth/managers.py @@ -10,6 +10,14 @@ DEFAULT_PRIVACY_LEVEL = getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') +class OAuthRepositoryManager(models.Manager): + pass + + +class OAuthOrganizationManager(models.Manager): + pass + + class GithubProjectManager(models.Manager): def create_from_api(self, api_json, user, organization=None, privacy=DEFAULT_PRIVACY_LEVEL): diff --git a/readthedocs/oauth/migrations/0002_combine_services.py b/readthedocs/oauth/migrations/0002_combine_services.py new file mode 100644 index 00000000000..2ecec67735e --- /dev/null +++ b/readthedocs/oauth/migrations/0002_combine_services.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import django.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('oauth', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='OAuthOrganization', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('pub_date', models.DateTimeField(auto_now_add=True, verbose_name='Publication date')), + ('modified_date', models.DateTimeField(auto_now=True, verbose_name='Modified date')), + ('active', models.BooleanField(default=False, verbose_name='Active')), + ('slug', models.CharField(unique=True, max_length=255, verbose_name='Slug')), + ('name', models.CharField(max_length=255, null=True, verbose_name='Name', blank=True)), + ('email', models.EmailField(max_length=255, null=True, verbose_name='Email', blank=True)), + ('avatar_url', models.URLField(null=True, verbose_name='Avatar image URL', blank=True)), + ('url', models.URLField(null=True, verbose_name='URL to organization page', blank=True)), + ('source', models.CharField(max_length=16, verbose_name='Repository source', choices=[(b'github', 'GitHub'), (b'bitbucket', 'Bitbucket')])), + ('json', models.TextField(verbose_name='Serialized API response')), + ('users', models.ManyToManyField(related_name='oauth_organizations', verbose_name='Users', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OAuthRepository', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('pub_date', models.DateTimeField(auto_now_add=True, verbose_name='Publication date')), + ('modified_date', models.DateTimeField(auto_now=True, verbose_name='Modified date')), + ('active', models.BooleanField(default=False, verbose_name='Active')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('full_name', models.CharField(max_length=255, verbose_name='Full Name')), + ('description', models.TextField(help_text='Description of the project', null=True, verbose_name='Description', blank=True)), + ('avatar_url', models.URLField(null=True, verbose_name='Owner avatar image URL', blank=True)), + ('ssh_url', models.URLField(blank=True, max_length=512, verbose_name='SSH URL', validators=[django.core.validators.URLValidator(schemes=[b'ssh'])])), + ('clone_url', models.URLField(blank=True, max_length=512, verbose_name='Repository clone URL', validators=[django.core.validators.URLValidator(schemes=[b'http', b'https', b'ssh', b'git', b'svn'])])), + ('html_url', models.URLField(null=True, verbose_name='HTML URL', blank=True)), + ('private', models.BooleanField(default=False, verbose_name='Private repository')), + ('admin', models.BooleanField(default=False, verbose_name='Has admin privilege')), + ('vcs', models.CharField(blank=True, max_length=200, verbose_name='vcs', choices=[(b'git', 'Git'), (b'svn', 'Subversion'), (b'hg', 'Mercurial'), (b'bzr', 'Bazaar')])), + ('source', models.CharField(max_length=16, verbose_name='Repository source', choices=[(b'github', 'GitHub'), (b'bitbucket', 'Bitbucket')])), + ('json', models.TextField(verbose_name='Serialized API response')), + ('organization', models.ForeignKey(related_name='repositories', verbose_name='Organization', blank=True, to='oauth.OAuthOrganization', null=True)), + ('users', models.ManyToManyField(related_name='oauth_repositories', verbose_name='Users', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['organization__name', 'name'], + }, + ), + ] diff --git a/readthedocs/oauth/models.py b/readthedocs/oauth/models.py index 61401f99c41..eb609a81792 100644 --- a/readthedocs/oauth/models.py +++ b/readthedocs/oauth/models.py @@ -1,11 +1,122 @@ +"""OAuth service models""" + from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ +from django.core.validators import URLValidator + +from readthedocs.projects.constants import REPO_CHOICES +from .constants import OAUTH_SOURCE from .managers import BitbucketTeamManager from .managers import BitbucketProjectManager from .managers import GithubOrganizationManager from .managers import GithubProjectManager +from .managers import OAuthRepositoryManager, OAuthOrganizationManager + + +class OAuthOrganization(models.Model): + + """Organization from OAuth service + + This encapsulates both Github and Bitbucket + """ + + # Auto fields + pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) + modified_date = models.DateTimeField(_('Modified date'), auto_now=True) + + users = models.ManyToManyField(User, verbose_name=_('Users'), + related_name='oauth_organizations') + active = models.BooleanField(_('Active'), default=False) + + slug = models.CharField(_('Slug'), max_length=255, unique=True) + name = models.CharField(_('Name'), max_length=255, null=True, blank=True) + email = models.EmailField(_('Email'), max_length=255, null=True, blank=True) + avatar_url = models.URLField(_('Avatar image URL'), null=True, blank=True) + url = models.URLField(_('URL to organization page'), max_length=200, + null=True, blank=True) + + source = models.CharField(_('Repository source'), max_length=16, + choices=OAUTH_SOURCE) + json = models.TextField(_('Serialized API response')) + + objects = OAuthOrganizationManager() + + def __unicode__(self): + return "OAuth Organization: %s" % (self.url) + + def get_serialized(self, key=None, default=None): + try: + data = json.loads(self.json) + if key is not None: + return data.get(key, default) + return data + except ValueError: + pass + + +class OAuthRepository(models.Model): + + """OAuth importable repositories + + This models Github and Bitbucket importable repositories + """ + + # Auto fields + pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) + modified_date = models.DateTimeField(_('Modified date'), auto_now=True) + + # This should now be a OneToOne + users = models.ManyToManyField(User, verbose_name=_('Users'), + related_name='oauth_repositories') + organization = models.ForeignKey( + OAuthOrganization, verbose_name=_('Organization'), + related_name='repositories', null=True, blank=True) + active = models.BooleanField(_('Active'), default=False) + + name = models.CharField(_('Name'), max_length=255) + full_name = models.CharField(_('Full Name'), max_length=255) + description = models.TextField(_('Description'), blank=True, null=True, + help_text=_('Description of the project')) + avatar_url = models.URLField(_('Owner avatar image URL'), null=True, + blank=True) + + ssh_url = models.URLField(_('SSH URL'), max_length=512, blank=True, + validators=[URLValidator(schemes=['ssh'])]) + clone_url = models.URLField( + _('Repository clone URL'), + max_length=512, + blank=True, + validators=[ + URLValidator(schemes=['http', 'https', 'ssh', 'git', 'svn'])]) + html_url = models.URLField(_('HTML URL'), null=True, blank=True) + + private = models.BooleanField(_('Private repository'), default=False) + admin = models.BooleanField(_('Has admin privilege'), default=False) + vcs = models.CharField(_('vcs'), max_length=200, blank=True, + choices=REPO_CHOICES) + + source = models.CharField(_('Repository source'), max_length=16, + choices=OAUTH_SOURCE) + json = models.TextField(_('Serialized API response')) + + objects = OAuthRepositoryManager() + + class Meta: + ordering = ['organization__name', 'name'] + + def __unicode__(self): + return "OAuth importable repository: %s" % (self.html_url) + + def get_serialized(self, key=None, default=None): + try: + data = json.loads(self.json) + if key is not None: + return data.get(key, default) + return data + except ValueError: + pass class GithubOrganization(models.Model): From 89fa008424c5ca17b50da6b7e3dff5f5a36fd7c7 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 18 Sep 2015 11:53:00 -0700 Subject: [PATCH 04/52] Add data migration for OAuth models --- .../oauth/migrations/0003_move_github.py | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 readthedocs/oauth/migrations/0003_move_github.py diff --git a/readthedocs/oauth/migrations/0003_move_github.py b/readthedocs/oauth/migrations/0003_move_github.py new file mode 100644 index 00000000000..5301d39224d --- /dev/null +++ b/readthedocs/oauth/migrations/0003_move_github.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import json + +from django.db import models, migrations + + +def forwards_move_repos(apps, schema_editor): + """Moves OAuth repos""" + db = schema_editor.connection.alias + + # Organizations + GithubOrganization = apps.get_model('oauth', 'GithubOrganization') + BitbucketTeam = apps.get_model('oauth', 'BitbucketTeam') + OAuthOrganization = apps.get_model('oauth', 'OAuthOrganization') + for org in GithubOrganization.objects.all(): + new_org = OAuthOrganization.objects.using(db).create( + pub_date=org.pub_date, + modified_date=org.modified_date, + active=org.active, + slug=org.login, + name=org.name, + email=org.email, + url=org.html_url, + source='github', + ) + for user in org.users.all(): + new_org.users.add(user) + try: + data = eval(org.json) + new_org.avatar_url = data['avatar_url'] + new_org.json = json.dumps(data) + except: + pass + new_org.save() + + for org in BitbucketTeam.objects.all(): + new_org = OAuthOrganization.objects.using(db).create( + pub_date=org.pub_date, + modified_date=org.modified_date, + active=org.active, + slug=org.login, + name=org.name, + email=org.email, + url=org.html_url, + source='bitbucket', + ) + for user in org.users.all(): + new_org.users.add(user) + try: + new_org.json = json.dumps(eval(org.json)) + except: + pass + new_org.save() + + # Now repositories + GithubProject = apps.get_model('oauth', 'GithubProject') + BitbucketProject = apps.get_model('oauth', 'BitbucketProject') + OAuthRepository = apps.get_model('oauth', 'OAuthRepository') + + for project in GithubProject.objects.all(): + new_repo = OAuthRepository.objects.using(db).create( + pub_date=project.pub_date, + modified_date=project.modified_date, + active=project.active, + name=project.name, + full_name=project.full_name, + description=project.description, + ssh_url=project.ssh_url, + clone_url=project.git_url, + html_url=project.html_url, + vcs='git', + source='github', + ) + for user in project.users.all(): + new_repo.users.add(user) + if project.organization is not None: + new_repo.organization = (OAuthOrganization + .objects + .using(db) + .get(slug=project.organization.login)) + try: + data = eval(project.json) + new_repo.avatar_url = data.get('owner', {}).get('avatar_url', None) + new_repo.admin = data.get('permissions', {}).get('admin', False) + new_repo.private = data.get('private', False) + new_repo.json = json.dumps(data) + except: + pass + new_repo.save() + + for project in BitbucketProject.objects.all(): + new_repo = OAuthRepository.objects.using(db).create( + pub_date=project.pub_date, + modified_date=project.modified_date, + active=project.active, + name=project.name, + full_name=project.full_name, + description=project.description, + ssh_url=project.ssh_url, + clone_url=project.git_url, + html_url=project.html_url, + admin=False, + vcs=project.vcs, + source='bitbucket', + ) + for user in project.users.all(): + new_repo.users.add(user) + if project.organization is not None: + new_repo.organization = (OAuthOrganization + .objects + .using(db) + .get(slug=project.organization.login)) + try: + data = eval(project.json) + new_repo.avatar_url = data.get('avatar', {}).get('href', None) + new_repo.private = data.get('is_private', False) + new_repo.json = json.dumps(data) + except: + pass + new_repo.save() + + +def reverse_move_repos(apps, schema_editor): + """Drop OAuth repos""" + db = schema_editor.connection.alias + OAuthRepository = apps.get_model('oauth', 'OAuthRepository') + OAuthOrganization = apps.get_model('oauth', 'OAuthOrganization') + OAuthRepository.objects.using(db).delete() + OAuthOrganization.objects.using(db).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth', '0002_combine_services'), + ] + + operations = [ + migrations.RunPython(forwards_move_repos, reverse_move_repos), + ] From 6174067e237b56bb809d92f470d6b3bb6eebcd53 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 18 Sep 2015 11:53:11 -0700 Subject: [PATCH 05/52] Add missing constants --- readthedocs/oauth/constants.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 readthedocs/oauth/constants.py diff --git a/readthedocs/oauth/constants.py b/readthedocs/oauth/constants.py new file mode 100644 index 00000000000..263a3557608 --- /dev/null +++ b/readthedocs/oauth/constants.py @@ -0,0 +1,12 @@ +"""Constants used for OAuth services""" + +from django.utils.translation import ugettext_lazy as _ + + +OAUTH_SOURCE_GITHUB = 'github' +OAUTH_SOURCE_BITBUCKET = 'bitbucket' + +OAUTH_SOURCE = ( + (OAUTH_SOURCE_GITHUB, _('GitHub')), + (OAUTH_SOURCE_BITBUCKET, _('Bitbucket')), +) From 6141c79832169fe6a9cb72e60d86c6e1c81c8324 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 18 Sep 2015 17:17:32 -0700 Subject: [PATCH 06/52] Redo html layout and add styling to import page --- gulpfile.js | 5 +- .../core/static-src/core/js/projectimport.js | 29 ++-- .../core/static/core/js/projectimport.js | 2 +- .../static-src/projects/css/import.less | 90 ++++++++++ .../projects/static/projects/css/import.css | 73 ++++++++ readthedocs/restapi/serializers.py | 33 +--- readthedocs/restapi/urls.py | 11 +- readthedocs/restapi/views/model_views.py | 34 +--- .../projects/project_import_from_service.html | 6 +- .../projects/project_import_github.html | 157 ++++++++++++------ 10 files changed, 310 insertions(+), 130 deletions(-) create mode 100644 readthedocs/projects/static-src/projects/css/import.less create mode 100644 readthedocs/projects/static/projects/css/import.css diff --git a/gulpfile.js b/gulpfile.js index 245b906420a..974a5268fb7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -40,7 +40,10 @@ var sources = { 'font/fontawesome-webfont.woff': {src: 'bower_components/font-awesome/fonts/fontawesome-webfont.woff'}, 'font/FontAwesome.otf': {src: 'bower_components/font-awesome/fonts/FontAwesome.otf'} }, - projects: {'js/tools.js': {}}, + projects: { + 'js/tools.js': {}, + 'css/import.less': {}, + }, gold: {'js/gold.js': {}}, donate: {'js/donate.js': {}} }; diff --git a/readthedocs/core/static-src/core/js/projectimport.js b/readthedocs/core/static-src/core/js/projectimport.js index a95b85b36a7..1395bd61edb 100644 --- a/readthedocs/core/static-src/core/js/projectimport.js +++ b/readthedocs/core/static-src/core/js/projectimport.js @@ -61,25 +61,16 @@ function Organization (instance) { var self = this; self.id = ko.observable(instance.id); self.name = ko.observable(instance.name); - self.login = ko.observable(instance.login); + self.slug = ko.observable(instance.slug); self.active = ko.observable(instance.active); self.avatar_url = ko.observable( append_url_params(instance.avatar_url, {size: 32}) ); self.display_name = ko.computed(function () { - return self.name() || self.login(); + return self.name() || self.slug(); }); } -function Owner (instance) { - var self = this; - self.name = ko.observable(instance.name); - self.avatar_url = ko.observable( - append_url_params(instance.avatar_url, {size: 32}) - ); - self.login = ko.observable(instance.login); -} - function Project (instance) { var self = this; self.id = ko.observable(instance.id); @@ -89,10 +80,18 @@ function Project (instance) { if (instance.organization) { self.organization(new Organization(instance.organization)); } - self.owner = ko.observable(new Owner(instance.owner)); - self.url = ko.observable(instance.url); + self.http_url = ko.observable(instance.http_url); + self.clone_url = ko.observable(instance.clone_url); + self.ssh_url = ko.observable(instance.ssh_url); self.private = ko.observable(instance.private); self.active = ko.observable(instance.active); + self.avatar_url = ko.observable( + append_url_params(instance.avatar_url, {size: 32}) + ); + + self.import_repo = function () { + alert('FUCK'); + }; } function ProjectImportView (instance, urls) { @@ -128,7 +127,7 @@ function ProjectImportView (instance, urls) { ko.computed(function () { var org = self.filter_org(), orgs = self.organizations(), - url = self.page_current() || self.urls['githubproject-list']; + url = self.page_current() || self.urls['oauthrepository-list']; if (org) { url = append_url_params(url, {org: org}); @@ -153,7 +152,7 @@ function ProjectImportView (instance, urls) { }); self.get_organizations = function () { - $.getJSON(self.urls['githuborganization-list']) + $.getJSON(self.urls['oauthorganization-list']) .success(function (organizations) { self.organizations_raw(organizations.results); }) diff --git a/readthedocs/core/static/core/js/projectimport.js b/readthedocs/core/static/core/js/projectimport.js index 5291d852ded..0bff1e73daa 100644 --- a/readthedocs/core/static/core/js/projectimport.js +++ b/readthedocs/core/static/core/js/projectimport.js @@ -1 +1 @@ -require=function e(r,n,t){function o(i,s){if(!n[i]){if(!r[i]){var u="function"==typeof require&&require;if(!s&&u)return u(i,!0);if(a)return a(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var l=n[i]={exports:{}};r[i][0].call(l.exports,function(e){var n=r[i][1][e];return o(n?n:e)},l,l.exports,e,r,n,t)}return n[i].exports}for(var a="function"==typeof require&&require,i=0;i").attr("href",e).get(0);return Object.keys(r).map(function(e){n.search&&(n.search+="&"),n.search+=e+"="+r[e]}),n.href}var c=e("knockout"),l=e("jquery"),f=e("./tasks");e("./django-csrf.js"),l(function(){var e=l("#id_repo"),r=l("#id_repo_type");e.blur(function(){var n,t=e.val();switch(!0){case/^hg/.test(t):n="hg";break;case/^bzr/.test(t):case/launchpad/.test(t):n="bzr";break;case/trunk/.test(t):case/^svn/.test(t):n="svn";break;default:case/github/.test(t):case/(^git|\.git$)/.test(t):n="git"}r.val(n)}),l("[data-sync-repositories]").each(function(){})}),s.init=function(e,r,n){var t=new s(r,n);return t.get_organizations(),c.applyBindings(t,e),t},r.exports.ProjectImportView=s},{"./django-csrf.js":1,"./tasks":2,jquery:"jquery",knockout:"knockout"}]},{},[]); \ No newline at end of file +require=function e(r,n,t){function o(i,s){if(!n[i]){if(!r[i]){var u="function"==typeof require&&require;if(!s&&u)return u(i,!0);if(a)return a(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var l=n[i]={exports:{}};r[i][0].call(l.exports,function(e){var n=r[i][1][e];return o(n?n:e)},l,l.exports,e,r,n,t)}return n[i].exports}for(var a="function"==typeof require&&require,i=0;i").attr("href",e).get(0);return Object.keys(r).map(function(e){n.search&&(n.search+="&"),n.search+=e+"="+r[e]}),n.href}var u=e("knockout"),c=e("jquery"),l=e("./tasks");e("./django-csrf.js"),c(function(){var e=c("#id_repo"),r=c("#id_repo_type");e.blur(function(){var n,t=e.val();switch(!0){case/^hg/.test(t):n="hg";break;case/^bzr/.test(t):case/launchpad/.test(t):n="bzr";break;case/trunk/.test(t):case/^svn/.test(t):n="svn";break;default:case/github/.test(t):case/(^git|\.git$)/.test(t):n="git"}r.val(n)}),c("[data-sync-repositories]").each(function(){})}),i.init=function(e,r,n){var t=new i(r,n);return t.get_organizations(),u.applyBindings(t,e),t},r.exports.ProjectImportView=i},{"./django-csrf.js":1,"./tasks":2,jquery:"jquery",knockout:"knockout"}]},{},[]); \ No newline at end of file diff --git a/readthedocs/projects/static-src/projects/css/import.less b/readthedocs/projects/static-src/projects/css/import.less new file mode 100644 index 00000000000..e20bbd4f783 --- /dev/null +++ b/readthedocs/projects/static-src/projects/css/import.less @@ -0,0 +1,90 @@ +div.project-import-remote { + h1 { + font-size: 1.2em; + margin: 0em 0em .5em 0em; + } + + button.remote-sync { + float: right; + margin: 0em; + } + + li.remote-repo { + padding-left: 3em; + padding-right: 3em; + + img.remote-repo-avatar { + float: left; + margin-left: -2.25em; + width: 1.5em; + height: 1.5em; + + border-radius: .2em; + -moz-border-radius: .2em; + -ms-border-radius: .2em; + -webkit-border-radius: .2em; + } + + div.remote-repo-info { + height: 2.625em; + overflow: hidden; + + a.remote-repo-name { + line-height: 1.5em; + text-decoration: none; + } + + span.remote-repo-url { + display: block; + font-size: .75em; + line-height: 1.5em; + color: #999; + } + } + + ul.remote-repo-menu { + top: 10px; + right: 10px; + + button.remote-repo-import { + font-size: 1em; + margin: .25em 0em; + width: 2.25em; + height: 2.125em; + } + } + } +} + +div.import-remote-sidebar { + h1, h2, h3 { + font-size: 1.1em; + } + + li.remote-org { + margin: .5em 0em; + padding-left: 3em; + + img.remote-org-avatar { + float: left; + margin-left: -2.25em; + margin-right: .5em; + width: 1.5em; + height: 1.5em; + + border-radius: .2em; + -moz-border-radius: .2em; + -ms-border-radius: .2em; + -webkit-border-radius: .2em; + + vertical-align: baseline; + } + + a.remote-org-name { + font-size: 1em; + text-decoration: none; + line-height: 1.5em; + vertical-align: baseline; + } + } +} diff --git a/readthedocs/projects/static/projects/css/import.css b/readthedocs/projects/static/projects/css/import.css new file mode 100644 index 00000000000..f5eeee40500 --- /dev/null +++ b/readthedocs/projects/static/projects/css/import.css @@ -0,0 +1,73 @@ +div.project-import-remote h1 { + font-size: 1.2em; + margin: 0em 0em .5em 0em; +} +div.project-import-remote button.remote-sync { + float: right; + margin: 0em; +} +div.project-import-remote li.remote-repo { + padding-left: 3em; + padding-right: 3em; +} +div.project-import-remote li.remote-repo img.remote-repo-avatar { + float: left; + margin-left: -2.25em; + width: 1.5em; + height: 1.5em; + border-radius: .2em; + -moz-border-radius: .2em; + -ms-border-radius: .2em; + -webkit-border-radius: .2em; +} +div.project-import-remote li.remote-repo div.remote-repo-info { + height: 2.625em; + overflow: hidden; +} +div.project-import-remote li.remote-repo div.remote-repo-info a.remote-repo-name { + line-height: 1.5em; + text-decoration: none; +} +div.project-import-remote li.remote-repo div.remote-repo-info span.remote-repo-url { + display: block; + font-size: .75em; + line-height: 1.5em; + color: #999; +} +div.project-import-remote li.remote-repo ul.remote-repo-menu { + top: 10px; + right: 10px; +} +div.project-import-remote li.remote-repo ul.remote-repo-menu button.remote-repo-import { + font-size: 1em; + margin: .25em 0em; + width: 2.25em; + height: 2.125em; +} +div.import-remote-sidebar h1, +div.import-remote-sidebar h2, +div.import-remote-sidebar h3 { + font-size: 1.1em; +} +div.import-remote-sidebar li.remote-org { + margin: .5em 0em; + padding-left: 3em; +} +div.import-remote-sidebar li.remote-org img.remote-org-avatar { + float: left; + margin-left: -2.25em; + margin-right: .5em; + width: 1.5em; + height: 1.5em; + border-radius: .2em; + -moz-border-radius: .2em; + -ms-border-radius: .2em; + -webkit-border-radius: .2em; + vertical-align: baseline; +} +div.import-remote-sidebar li.remote-org a.remote-org-name { + font-size: 1em; + text-decoration: none; + line-height: 1.5em; + vertical-align: baseline; +} diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index fb568aaceeb..99bd3406fb0 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -2,8 +2,7 @@ from readthedocs.builds.models import Build, BuildCommandResult, Version from readthedocs.projects.models import Project, Domain -from readthedocs.oauth.models import (GithubOrganization, GithubProject, - BitbucketTeam, BitbucketProject) +from readthedocs.oauth.models import OAuthOrganization, OAuthRepository class ProjectSerializer(serializers.ModelSerializer): @@ -84,37 +83,19 @@ class Meta: ) -class GithubOrganizationSerializer(serializers.ModelSerializer): - - avatar_url = serializers.ReadOnlyField() +class OAuthOrganizationSerializer(serializers.ModelSerializer): class Meta: - model = GithubOrganization + model = OAuthOrganization exclude = ('json', 'email', 'users') -class GithubProjectSerializer(serializers.ModelSerializer): +class OAuthRepositorySerializer(serializers.ModelSerializer): - """Github project serializer""" + """OAuth service repository serializer""" - private = serializers.ReadOnlyField(source='is_private') - owner = serializers.ReadOnlyField() - organization = GithubOrganizationSerializer() + organization = OAuthOrganizationSerializer() class Meta: - model = GithubProject + model = OAuthRepository exclude = ('json', 'users') - - -class BitbucketTeamSerializer(serializers.ModelSerializer): - - class Meta: - model = BitbucketTeam - exclude = ('json',) - - -class BitbucketProjectSerializer(serializers.ModelSerializer): - - class Meta: - model = BitbucketProject - exclude = ('json',) diff --git a/readthedocs/restapi/urls.py b/readthedocs/restapi/urls.py index 7355a3dad37..f00b2c7746c 100644 --- a/readthedocs/restapi/urls.py +++ b/readthedocs/restapi/urls.py @@ -4,9 +4,8 @@ from .views.model_views import (BuildViewSet, BuildCommandViewSet, ProjectViewSet, NotificationViewSet, - VersionViewSet, DomainViewSet - GithubOrganizationViewSet, GithubProjectViewSet, - BitbucketTeamViewSet, BitbucketProjectViewSet) + VersionViewSet, DomainViewSet, + OAuthOrganizationViewSet, OAuthRepositoryViewSet) from readthedocs.comments.views import CommentViewSet router = routers.DefaultRouter() @@ -16,10 +15,8 @@ router.register(r'project', ProjectViewSet) router.register(r'notification', NotificationViewSet) router.register(r'domain', DomainViewSet) -router.register(r'github/org', GithubOrganizationViewSet) -router.register(r'github/project', GithubProjectViewSet) -router.register(r'bitbucket/team', BitbucketTeamViewSet) -router.register(r'bitbucket/project', BitbucketProjectViewSet) +router.register(r'oauth/org', OAuthOrganizationViewSet) +router.register(r'oauth/repo', OAuthRepositoryViewSet) router.register(r'comments', CommentViewSet, base_name="comments") urlpatterns = patterns( diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py index a781f87dd59..8df21348fdb 100644 --- a/readthedocs/restapi/views/model_views.py +++ b/readthedocs/restapi/views/model_views.py @@ -13,8 +13,7 @@ from readthedocs.restapi import utils as api_utils from readthedocs.core.utils import trigger_build from readthedocs.oauth import utils as oauth_utils -from readthedocs.oauth.models import (GithubOrganization, GithubProject, - BitbucketTeam, BitbucketProject) +from readthedocs.oauth.models import OAuthOrganization, OAuthRepository from readthedocs.builds.constants import STABLE from readthedocs.projects.filters import ProjectFilter, DomainFilter from readthedocs.projects.models import Project, EmailHook, Domain @@ -25,8 +24,7 @@ from ..serializers import (BuildSerializerFull, BuildSerializer, BuildCommandSerializer, ProjectSerializer, VersionSerializer, DomainSerializer, - GithubOrganizationSerializer, GithubProjectSerializer, - BitbucketTeamSerializer, BitbucketProjectSerializer) + OAuthOrganizationSerializer, OAuthRepositorySerializer) from .. import utils as api_utils log = logging.getLogger(__name__) @@ -220,21 +218,21 @@ def get_queryset(self): return self.model.objects.filter(users=self.request.user) -class GithubOrganizationViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): +class OAuthOrganizationViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): permission_classes = [APIPermission] renderer_classes = [JSONRenderer, BrowsableAPIRenderer] - serializer_class = GithubOrganizationSerializer - model = GithubOrganization + serializer_class = OAuthOrganizationSerializer + model = OAuthOrganization -class GithubProjectViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): +class OAuthRepositoryViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): permission_classes = [APIPermission] renderer_classes = [JSONRenderer, BrowsableAPIRenderer] - serializer_class = GithubProjectSerializer - model = GithubProject + serializer_class = OAuthRepositorySerializer + model = OAuthRepository def get_queryset(self): - query = super(GithubProjectViewSet, self).get_queryset() + query = super(OAuthRepositoryViewSet, self).get_queryset() org = self.request.query_params.get('org', None) if org is not None: query = query.filter(organization__pk=org) @@ -242,17 +240,3 @@ def get_queryset(self): def get_paginate_by(self): return self.request.query_params.get('page_size', 25) - - -class BitbucketProjectViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): - permission_classes = [APIPermission] - renderer_classes = [JSONRenderer, BrowsableAPIRenderer] - serializer_class = BitbucketProjectSerializer - model = BitbucketProject - - -class BitbucketTeamViewSet(OAuthServiceMixin, viewsets.ReadOnlyModelViewSet): - permission_classes = [APIPermission] - renderer_classes = [JSONRenderer, BrowsableAPIRenderer] - serializer_class = BitbucketTeamSerializer - model = BitbucketTeam diff --git a/readthedocs/templates/projects/project_import_from_service.html b/readthedocs/templates/projects/project_import_from_service.html index b9c489ec72c..f139b25cb8e 100644 --- a/readthedocs/templates/projects/project_import_from_service.html +++ b/readthedocs/templates/projects/project_import_from_service.html @@ -17,10 +17,8 @@ var urls = { 'api_sync_github_repositories': '{% url 'api_sync_github_repositories' %}', 'api_sync_bitbucket_repositories': '{% url 'api_sync_bitbucket_repositories' %}', - 'bitbucketteam-list': '{% url 'bitbucketteam-list' %}', - 'bitbucketproject-list': '{% url 'bitbucketproject-list' %}', - 'githuborganization-list': '{% url 'githuborganization-list' %}', - 'githubproject-list': '{% url 'githubproject-list' %}', + 'oauthorganization-list': '{% url 'oauthorganization-list' %}', + 'oauthrepository-list': '{% url 'oauthrepository-list' %}', }, instance = {}, {# projects_json|safe, #} view = import_views.ProjectImportView.init( diff --git a/readthedocs/templates/projects/project_import_github.html b/readthedocs/templates/projects/project_import_github.html index e5aaf012afa..8fae271434c 100644 --- a/readthedocs/templates/projects/project_import_github.html +++ b/readthedocs/templates/projects/project_import_github.html @@ -1,62 +1,98 @@ -{% extends "projects/project_import_from_service.html" %} +{% extends "base.html" %} +{% load static %} {% load i18n %} +{% block title %}{% trans "Import a Remote Repository" %}{% endblock %} -{% block title %}{% trans "Import a GitHub project" %}{% endblock %} +{% block extra_links %} + +{% endblock %} + +{% block extra_scripts %} + + + +{% endblock %} -{% block content-header %}

{% trans "Import a GitHub project" %}

{% endblock %} {% block content %} {% if github_connected %} - {% block top_content %} - {% url 'api_sync_github_repositories' as sync_url %} - - {% endblock top_content %} - -