diff --git a/.gitignore b/.gitignore index 38499d2..608d5bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +!.gitignore +.* *.pyc .DS_Store .env diff --git a/ghapi/api.py b/ghapi/api.py index ec43902..599d9bb 100644 --- a/ghapi/api.py +++ b/ghapi/api.py @@ -25,8 +25,11 @@ class GitHub(object): """ Gets a resource, eg 'users/ojii'. - Returns tuple (jsondata, response) + Returns parsed json data as a python dictionary """ + return self._get(path, params)[0] + + def _get(self, path, params=None): if params is None: params = {} params['per_page'] = 100 @@ -34,12 +37,13 @@ class GitHub(object): response.raise_for_status() return response.json, response + def get_iter(self, path, params=None): """ Returns an iterator over a resource, eg 'repos/divio/django-cms/watchers' that automatically handles pagination. """ - data, response = self.get(path, params) + data, response = self._get(path, params) for thing in data: yield thing next_page = get_next_page(response) diff --git a/githubnetwork/admin.py b/githubnetwork/admin.py new file mode 100644 index 0000000..07729db --- /dev/null +++ b/githubnetwork/admin.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin +from githubnetwork.models import GHUser, Repo + + +admin.site.register(GHUser) +admin.site.register(Repo) diff --git a/githubnetwork/management/__init__.py b/githubnetwork/management/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/githubnetwork/management/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/githubnetwork/management/commands/__init__.py b/githubnetwork/management/commands/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/githubnetwork/management/commands/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/githubnetwork/management/commands/make_admin.py b/githubnetwork/management/commands/make_admin.py new file mode 100644 index 0000000..fd7b98c --- /dev/null +++ b/githubnetwork/management/commands/make_admin.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, username, **option): + User.objects.filter(username=username).update(is_staff=True, is_superuser=True) diff --git a/githubnetwork/management/commands/revoke_admin.py b/githubnetwork/management/commands/revoke_admin.py new file mode 100644 index 0000000..ede277f --- /dev/null +++ b/githubnetwork/management/commands/revoke_admin.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, username, **option): + User.objects.filter(username=username).update(is_staff=False, is_superuser=False) diff --git a/githubnetwork/migrations/0001_initial.py b/githubnetwork/migrations/0001_initial.py new file mode 100644 index 0000000..413dce8 --- /dev/null +++ b/githubnetwork/migrations/0001_initial.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'GHUser' + db.create_table('githubnetwork_ghuser', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('last_sync', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True, null=True, blank=True)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + ('acct_type', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('gh_login', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('blog', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=255, blank=True)), + ('avatar_url', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)), + ('public_gists', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('hireable', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('followers_count', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('html_url', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)), + ('bio', self.gf('django.db.models.fields.TextField')(blank=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('company', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('url', self.gf('django.db.models.fields.URLField')(max_length=255, blank=True)), + ('gravatar_id', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('gh_id', self.gf('django.db.models.fields.IntegerField')(default=-1)), + ('public_repos', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('following_count', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('location', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('complete', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('githubnetwork', ['GHUser']) + + # Adding M2M table for field following on 'GHUser' + db.create_table('githubnetwork_ghuser_following', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('from_ghuser', models.ForeignKey(orm['githubnetwork.ghuser'], null=False)), + ('to_ghuser', models.ForeignKey(orm['githubnetwork.ghuser'], null=False)) + )) + db.create_unique('githubnetwork_ghuser_following', ['from_ghuser_id', 'to_ghuser_id']) + + # Adding model 'Repo' + db.create_table('githubnetwork_repo', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('last_sync', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['githubnetwork.GHUser'], unique=True)), + ('forks', self.gf('django.db.models.fields.IntegerField')()), + ('language', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')()), + ('open_issues', self.gf('django.db.models.fields.IntegerField')()), + ('description', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('ssh_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('has_downloads', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('svn_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('has_wiki', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('html_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('watchers', self.gf('django.db.models.fields.IntegerField')()), + ('size', self.gf('django.db.models.fields.IntegerField')()), + ('full_name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('clone_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('git_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('mirror_url', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('has_issues', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('homepage', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('private', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('gh_repo_id', self.gf('django.db.models.fields.IntegerField')()), + ('pushed_at', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal('githubnetwork', ['Repo']) + + + def backwards(self, orm): + # Deleting model 'GHUser' + db.delete_table('githubnetwork_ghuser') + + # Removing M2M table for field following on 'GHUser' + db.delete_table('githubnetwork_ghuser_following') + + # Deleting model 'Repo' + db.delete_table('githubnetwork_repo') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'githubnetwork.ghuser': { + 'Meta': {'object_name': 'GHUser'}, + 'acct_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'avatar_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'blog': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'company': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'followers_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'following': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'following_rel_+'", 'blank': 'True', 'to': "orm['githubnetwork.GHUser']"}), + 'following_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'gh_id': ('django.db.models.fields.IntegerField', [], {'default': '-1'}), + 'gh_login': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'gravatar_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'hireable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'html_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_sync': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'public_gists': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'public_repos': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'githubnetwork.repo': { + 'Meta': {'object_name': 'Repo'}, + 'clone_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'forks': ('django.db.models.fields.IntegerField', [], {}), + 'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'gh_repo_id': ('django.db.models.fields.IntegerField', [], {}), + 'git_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'has_downloads': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_issues': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'homepage': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'html_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'last_sync': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'mirror_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'open_issues': ('django.db.models.fields.IntegerField', [], {}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['githubnetwork.GHUser']", 'unique': 'True'}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pushed_at': ('django.db.models.fields.DateTimeField', [], {}), + 'size': ('django.db.models.fields.IntegerField', [], {}), + 'ssh_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'svn_url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'watchers': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['githubnetwork'] \ No newline at end of file diff --git a/githubnetwork/migrations/__init__.py b/githubnetwork/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/githubnetwork/models.py b/githubnetwork/models.py index 3ad076f..df22f5d 100644 --- a/githubnetwork/models.py +++ b/githubnetwork/models.py @@ -1,53 +1,115 @@ -# -*- coding: utf-8 -*- +import datetime +from django.contrib.auth.signals import user_logged_in from django.db import models from django.contrib.auth.models import User -class GHUser(models.Model): - user = models.ForeignKey(User, unique=True, verbose_name='ghuser') - following = models.ManyToMany(self, related_name='followers') - created_at = models.DateTimeField('date account created') - acct_type = models.CharField(max_length=255) - gh_login = models.CharField(max_length=255) - blog = models.URLfield(max_length=255) - email = models.EmailField(max_length=255) - avatar_url = models.URLfield(max_length=255) - public_gists = models.IntegerField() - hireable = models.BooleanField() - followers_count = models.IntegerField() - html_url = models.URLfield(max_length=255) - bio = models.TextField() - name = models.CharField(max_length=255) - company = models.CharField(max_length=255) - url = models.URLfield(max_length=255) - gravatar_id = models.CharField(max_length=255) - gh_id = models.IntegerField() - public_repos = models.IntegerField() - following_count = models.IntegerField() - location = models.CharField(max_length=255) + +class BaseAPIModel(models.Model): + last_sync = models.DateTimeField(default=datetime.datetime.now) + + class Meta: + abstract = True + + def needs_refresh(self): + now = datetime.datetime.now() + return not self.complete or now - self.last_sync > datetime.timedelta(days=7) + + def refresh(self, api): + """ + Refresh the data on this object + """ + pass # TODO + + +class GHUserManager(models.Manager): + def create_from_api(self, username, api): + obj = self.model(complete=True, gh_login=username) + obj.refresh(api) + return obj + + +class GHUser(BaseAPIModel): + user = models.ForeignKey(User, unique=True, null=True, blank=True) + following = models.ManyToManyField('self', related_name='followers', blank=True) + created_at = models.DateTimeField('date account created', blank=True, null=True ) + acct_type = models.CharField(max_length=255, blank=True) + gh_login = models.CharField(max_length=255, unique=True) + blog = models.URLField(max_length=255, blank=True) + email = models.EmailField(max_length=255, blank=True) + avatar_url = models.URLField(max_length=255, blank=True) + public_gists = models.IntegerField(default=0) + hireable = models.BooleanField(default=False) + followers_count = models.IntegerField(default=0) + html_url = models.URLField(max_length=255, blank=True) + bio = models.TextField(blank=True) + name = models.CharField(max_length=255, blank=True) + company = models.CharField(max_length=255, blank=True) + url = models.URLField(max_length=255, blank=True) + gravatar_id = models.CharField(max_length=255, blank=True) + gh_id = models.IntegerField(default=-1) + public_repos = models.IntegerField(default=0) + following_count = models.IntegerField(default=0) + location = models.CharField(max_length=255, blank=True) + complete = models.BooleanField(default=False) + + objects = GHUserManager() def __unicode__(self): - return self.login + return self.gh_login -class Repo(models.Model): + def _translate(self, data): + data['acct_type'] = data.pop('type', '') + data['followers_count'] = data.pop('followers', '0') + data['following_count'] = data.pop('following', '0') + data['gh_login'] = data.pop('login') + data['gh_id'] = data.pop('id', '0') + return data + + def refresh(self, api): + data = api.get('users/%s' % self.gh_login) + data = self._translate(data) + for key, value in data.items(): + setattr(self, key, value) + self.complete = True + self.save() + # set followers/following + cache = {} + def inner(iter): + users = [] + for shortuser in iter: + user = cache.get(shortuser['login'], None) + if not user: + try: + user = GHUser.objects.get(gh_login=shortuser['login']) + except self.DoesNotExist: + user = GHUser.objects.create(**self._translate(shortuser)) + users.append(user) + cache[user.gh_login] = user + self.followers = inner(api.get_iter('users/%s/followers' % self.gh_login)) + self.following = inner(api.get_iter('users/%s/followers' % self.gh_login)) + return self + + +class Repo(BaseAPIModel): owner = models.ForeignKey(GHUser, unique=True, verbose_name='ghuser') forks = models.IntegerField() language = models.CharField(max_length=255) created_at = models.DateTimeField('date repo created') open_issues = models.IntegerField() description = models.CharField(max_length=255) - ssh_url = models.URLfield(max_length=255) + ssh_url = models.URLField(max_length=255) has_downloads = models.BooleanField() - svn_url = models.URLfield(max_length=255) - has_wiki = models.BooleanField - html_url = models.URLfield(max_length=255) + svn_url = models.URLField(max_length=255) + has_wiki = models.BooleanField() + html_url = models.URLField(max_length=255) watchers = models.IntegerField() size = models.IntegerField() full_name = models.CharField(max_length=255) - clone_url = models.URLfield(max_length=255) - git_url = models.URLfield(max_length=255) + clone_url = models.URLField(max_length=255) + git_url = models.URLField(max_length=255) name = models.CharField(max_length=255) - url = models.URLfield(max_length=255) - mirror_url = models.URLfield(max_length=255) + url = models.URLField(max_length=255) + mirror_url = models.URLField(max_length=255) has_issues = models.BooleanField() homepage = models.CharField(max_length=255) private = models.BooleanField() @@ -55,4 +117,14 @@ class Repo(models.Model): pushed_at = models.DateTimeField('date repo pushed') def __unicode__(self): - return self.name + return self.name + +def update_user(request, user, **kwargs): + try: + ghuser = GHUser.objects.get(gh_login=user.username) + except GHUser.DoesNotExist: + return GHUser.objects.create_from_api(user.username, request.github) + ghuser.user = user + ghuser.refresh(request.github) + +user_logged_in.connect(update_user) diff --git a/settings.py b/settings.py index 23a7373..b66fea8 100644 --- a/settings.py +++ b/settings.py @@ -17,6 +17,7 @@ assert 'SECRET_KEY' in os.environ, 'Set SECRET_KEY in your .env file!' SECRET_KEY = os.environ['SECRET_KEY'] USE_L10N = USE_I18N = False +USE_TZ = True MEDIA_ROOT = os.path.join(PROJECT_DIR, 'media') MEDIA_URL = '/media/' @@ -75,11 +76,12 @@ INSTALLED_APPS = [ 'south', 'raven.contrib.django', 'social_auth', + # custom + 'githubnetwork', ] AUTHENTICATION_BACKENDS = [ 'social_auth.backends.contrib.github.GithubBackend', - 'django.contrib.auth.backends.ModelBackend', ] LOGIN_URL = '/login/' diff --git a/templates/login.html b/templates/login.html index 66cb19a..033e323 100644 --- a/templates/login.html +++ b/templates/login.html @@ -15,8 +15,12 @@
+ {% if request.user.is_authenticated %} +

Welcome back, {{ request.user.username }}

+ {% else %} Login with GitHub + {% endif %}
diff --git a/views.py b/views.py index 724e256..a536f83 100644 --- a/views.py +++ b/views.py @@ -12,7 +12,7 @@ def index(request): # Set a test cookie. When the user clicks the 'Login' button, test and make # sure this cookie was set properly. request.session.set_test_cookie() - return render_to_response('login.html') + return render_to_response('login.html', RequestContext(request)) def login(request):