Skip to content

Commit e495883

Browse files
committed
Merge pull request #1850 from rtfd/bitbucket-oauth2
Changes to support Bitbucket OAuth2
2 parents aba714e + 4b14539 commit e495883

26 files changed

+1124
-468
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
.idea
88
.vagrant
99
.tox
10+
.rope_project/
11+
.ropeproject/
1012
_build
1113
cnames
1214
bower_components/
@@ -35,4 +37,3 @@ whoosh_index
3537
xml_output
3638
public_cnames
3739
public_symlinks
38-
.rope_project/

media/css/core.css

+20-5
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,21 @@ div.search-sponsored { margin-top: 50px; }
348348
.module-item .module-item-menu li a:hover { background-color: #697983; box-shadow: 0 1px 0px #465158; -moz-box-shadow: 0 1px 0px #465158; -webkit-box-shadow: 0 1px 0px #465158; }
349349

350350
li.module-item ul.module-item-menu li input[type="submit"] {
351-
font-size: 16px;
352-
line-height: 16px;
353-
margin: 4px;
351+
font-size: 16px;
352+
line-height: 16px;
353+
margin: 4px;
354+
}
355+
356+
li.module-item > img {
357+
height: 24px;
358+
width: 24px;
359+
vertical-align: middle;
360+
}
361+
362+
li.module-item > p.error {
363+
padding-top: .75em;
364+
font-size: .8em;
365+
line-height: 1.5em;
354366
}
355367

356368
/* for links that span the column */
@@ -653,12 +665,15 @@ div.project-import-remote button.remote-sync:before {
653665
content: "\f021";
654666
}
655667

656-
div.project-import-remote form.import-connect-github button:before {
668+
div.project-import-remote form.import-connect-github button:before,
669+
a.socialaccount-provider.github:before {
657670
font-family: FontAwesome;
658671
content: "\f09b";
659672
}
660673

661-
div.project-import-remote form.import-connect-bitbucket button:before {
674+
div.project-import-remote form.import-connect-bitbucket button:before,
675+
a.socialaccount-provider.bitbucket:before,
676+
a.socialaccount-provider.bitbucket_oauth2:before {
662677
font-family: FontAwesome;
663678
content: "\f171";
664679
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from django.core.management.base import BaseCommand
22
from django.contrib.auth.models import User
33

4-
from readthedocs.oauth.utils import import_github
4+
from readthedocs.oauth.services import GitHubService
55

66

77
class Command(BaseCommand):
88

99
def handle(self, *args, **options):
1010
if len(args):
1111
for slug in args:
12-
import_github(user=User.objects.get(username=slug), sync=True)
12+
service = GitHubService.for_user(User.objects.get(username=slug))
13+
if service is not None:
14+
service.sync()

readthedocs/oauth/managers.py

+3-100
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,11 @@
1-
import logging
2-
import json
3-
import re
4-
5-
from django.conf import settings
6-
from django.db import models
1+
"""Managers for OAuth models"""
72

83
from readthedocs.privacy.loader import RelatedUserManager
94

10-
from .constants import OAUTH_SOURCE_GITHUB, OAUTH_SOURCE_BITBUCKET
11-
12-
13-
logger = logging.getLogger(__name__)
14-
15-
16-
DEFAULT_PRIVACY_LEVEL = getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public')
17-
185

196
class RemoteRepositoryManager(RelatedUserManager):
20-
21-
"""Model managers for remote repositories"""
22-
23-
def create_from_github_api(self, api_json, user, organization=None,
24-
privacy=DEFAULT_PRIVACY_LEVEL):
25-
logger.info('Trying GitHub: %s' % api_json['full_name'])
26-
if (
27-
(privacy == 'private') or
28-
(api_json['private'] is False and privacy == 'public')):
29-
project, created = self.get_or_create(
30-
full_name=api_json['full_name'],
31-
users__pk=user.pk,
32-
)
33-
if project.organization and project.organization != organization:
34-
logger.debug('Not importing %s because mismatched orgs' %
35-
api_json['name'])
36-
return None
37-
else:
38-
project.organization = organization
39-
project.users.add(user)
40-
project.name = api_json['name']
41-
project.description = api_json['description']
42-
project.ssh_url = api_json['ssh_url']
43-
project.html_url = api_json['html_url']
44-
project.private = api_json['private']
45-
if project.private:
46-
project.clone_url = api_json['ssh_url']
47-
else:
48-
project.clone_url = api_json['clone_url']
49-
project.admin = api_json.get('permissions', {}).get('admin', False)
50-
project.vcs = 'git'
51-
project.source = OAUTH_SOURCE_GITHUB
52-
project.avatar_url = api_json.get('owner', {}).get('avatar_url')
53-
project.json = json.dumps(api_json)
54-
project.save()
55-
return project
56-
else:
57-
logger.debug('Not importing %s because mismatched type' %
58-
api_json['name'])
59-
60-
def create_from_bitbucket_api(self, api_json, user, organization=None,
61-
privacy=DEFAULT_PRIVACY_LEVEL):
62-
logger.info('Trying Bitbucket: %s' % api_json['full_name'])
63-
if (api_json['is_private'] is True and privacy == 'private' or
64-
api_json['is_private'] is False and privacy == 'public'):
65-
project, created = self.get_or_create(
66-
full_name=api_json['full_name'])
67-
if project.organization and project.organization != organization:
68-
logger.debug('Not importing %s because mismatched orgs' %
69-
api_json['name'])
70-
return None
71-
else:
72-
project.organization = organization
73-
project.users.add(user)
74-
project.name = api_json['name']
75-
project.description = api_json['description']
76-
project.clone_url = api_json['links']['clone'][0]['href']
77-
project.ssh_url = api_json['links']['clone'][1]['href']
78-
project.html_url = api_json['links']['html']['href']
79-
project.vcs = api_json['scm']
80-
project.private = api_json['is_private']
81-
project.source = OAUTH_SOURCE_BITBUCKET
82-
83-
avatar_url = api_json['links']['avatar']['href'] or ''
84-
project.avatar_url = re.sub(r'\/16\/$', r'/32/', avatar_url)
85-
86-
project.json = json.dumps(api_json)
87-
project.save()
88-
return project
89-
else:
90-
logger.debug('Not importing %s because mismatched type' %
91-
api_json['name'])
7+
pass
928

939

9410
class RemoteOrganizationManager(RelatedUserManager):
95-
96-
def create_from_github_api(self, api_json, user):
97-
organization, created = self.get_or_create(slug=api_json.get('login'))
98-
organization.html_url = api_json.get('html_url')
99-
organization.name = api_json.get('name')
100-
organization.email = api_json.get('email')
101-
organization.avatar_url = api_json.get('avatar_url')
102-
organization.json = json.dumps(api_json)
103-
organization.users.add(user)
104-
organization.save()
105-
return organization
106-
107-
def create_from_bitbucket_api(self):
108-
pass
11+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('socialaccount', '0002_token_max_lengths'),
11+
('oauth', '0004_drop_github_and_bitbucket_models'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='remoteorganization',
17+
name='account',
18+
field=models.ForeignKey(related_name='remote_organizations', verbose_name='Connected account', blank=True, to='socialaccount.SocialAccount', null=True),
19+
),
20+
migrations.AddField(
21+
model_name='remoterepository',
22+
name='account',
23+
field=models.ForeignKey(related_name='remote_repositories', verbose_name='Connected account', blank=True, to='socialaccount.SocialAccount', null=True),
24+
),
25+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
6+
7+
def forwards_move_repo_source(apps, schema_editor):
8+
"""Use source field to set repository account"""
9+
RemoteRepository = apps.get_model('oauth', 'RemoteRepository')
10+
SocialAccount = apps.get_model('socialaccount', 'SocialAccount')
11+
for account in SocialAccount.objects.all():
12+
rows = (RemoteRepository.objects
13+
.filter(users=account.user, source=account.provider)
14+
.update(account=account))
15+
16+
17+
def backwards_move_repo_source(apps, schema_editor):
18+
RemoteRepository = apps.get_model('oauth', 'RemoteRepository')
19+
SocialAccount = apps.get_model('socialaccount', 'SocialAccount')
20+
for account in SocialAccount.objects.all():
21+
rows = (account.remote_repositories
22+
.update(account=None, source=account.provider))
23+
24+
25+
def forwards_move_org_source(apps, schema_editor):
26+
"""Use source field to set organization account"""
27+
RemoteOrganization = apps.get_model('oauth', 'RemoteOrganization')
28+
SocialAccount = apps.get_model('socialaccount', 'SocialAccount')
29+
for account in SocialAccount.objects.all():
30+
rows = (RemoteOrganization.objects
31+
.filter(users=account.user, source=account.provider)
32+
.update(account=account))
33+
34+
35+
def backwards_move_org_source(apps, schema_editor):
36+
"""Use source field to set organization account"""
37+
RemoteOrganization = apps.get_model('oauth', 'RemoteOrganization')
38+
SocialAccount = apps.get_model('socialaccount', 'SocialAccount')
39+
for account in SocialAccount.objects.all():
40+
rows = (account.remote_organizations
41+
.update(account=None, source=account.provider))
42+
43+
44+
class Migration(migrations.Migration):
45+
46+
dependencies = [
47+
('oauth', '0005_add_account_relation'),
48+
]
49+
50+
operations = [
51+
migrations.RunPython(forwards_move_repo_source,
52+
backwards_move_repo_source),
53+
migrations.RunPython(forwards_move_org_source,
54+
backwards_move_org_source),
55+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('oauth', '0006_move_oauth_source'),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name='remoteorganization',
16+
name='source',
17+
),
18+
migrations.RemoveField(
19+
model_name='remoterepository',
20+
name='source',
21+
),
22+
migrations.AlterField(
23+
model_name='remoteorganization',
24+
name='slug',
25+
field=models.CharField(max_length=255, verbose_name='Slug'),
26+
),
27+
]

readthedocs/oauth/models.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.utils.translation import ugettext_lazy as _
1010
from django.core.validators import URLValidator
1111
from django.core.urlresolvers import reverse
12+
from allauth.socialaccount.models import SocialAccount
1213

1314
from readthedocs.projects.constants import REPO_CHOICES
1415
from readthedocs.projects.models import Project
@@ -33,23 +34,24 @@ class RemoteOrganization(models.Model):
3334

3435
users = models.ManyToManyField(User, verbose_name=_('Users'),
3536
related_name='oauth_organizations')
37+
account = models.ForeignKey(
38+
SocialAccount, verbose_name=_('Connected account'),
39+
related_name='remote_organizations', null=True, blank=True)
3640
active = models.BooleanField(_('Active'), default=False)
3741

38-
slug = models.CharField(_('Slug'), max_length=255, unique=True)
42+
slug = models.CharField(_('Slug'), max_length=255)
3943
name = models.CharField(_('Name'), max_length=255, null=True, blank=True)
4044
email = models.EmailField(_('Email'), max_length=255, null=True, blank=True)
4145
avatar_url = models.URLField(_('Avatar image URL'), null=True, blank=True)
4246
url = models.URLField(_('URL to organization page'), max_length=200,
4347
null=True, blank=True)
4448

45-
source = models.CharField(_('Repository source'), max_length=16,
46-
choices=OAUTH_SOURCE)
4749
json = models.TextField(_('Serialized API response'))
4850

4951
objects = RemoteOrganizationManager()
5052

5153
def __unicode__(self):
52-
return "Remote Organization: %s" % (self.url)
54+
return 'Remote organization: {name}'.format(name=self.slug)
5355

5456
def get_serialized(self, key=None, default=None):
5557
try:
@@ -75,6 +77,9 @@ class RemoteRepository(models.Model):
7577
# This should now be a OneToOne
7678
users = models.ManyToManyField(User, verbose_name=_('Users'),
7779
related_name='oauth_repositories')
80+
account = models.ForeignKey(
81+
SocialAccount, verbose_name=_('Connected account'),
82+
related_name='remote_repositories', null=True, blank=True)
7883
organization = models.ForeignKey(
7984
RemoteOrganization, verbose_name=_('Organization'),
8085
related_name='repositories', null=True, blank=True)
@@ -102,8 +107,6 @@ class RemoteRepository(models.Model):
102107
vcs = models.CharField(_('vcs'), max_length=200, blank=True,
103108
choices=REPO_CHOICES)
104109

105-
source = models.CharField(_('Repository source'), max_length=16,
106-
choices=OAUTH_SOURCE)
107110
json = models.TextField(_('Serialized API response'))
108111

109112
objects = RemoteRepositoryManager()
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Conditional classes for OAuth services"""
2+
3+
from django.utils.module_loading import import_by_path
4+
from django.conf import settings
5+
6+
GitHubService = import_by_path(
7+
getattr(settings, 'OAUTH_GITHUB_SERVICE',
8+
'readthedocs.oauth.services.github.GitHubService'))
9+
BitbucketService = import_by_path(
10+
getattr(settings, 'OAUTH_BITBUCKET_SERVICE',
11+
'readthedocs.oauth.services.bitbucket.BitbucketService'))
12+
13+
registry = [GitHubService, BitbucketService]

0 commit comments

Comments
 (0)